-
Notifications
You must be signed in to change notification settings - Fork 71
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(compartment-mapper): Consistent hashing #799
Conversation
2bdf518
to
72f776e
Compare
103315e
to
83e2894
Compare
72f776e
to
44c653f
Compare
44c653f
to
d304485
Compare
2aae715
to
6d6f086
Compare
ef8c147
to
e0eed22
Compare
6d6f086
to
68f9289
Compare
e0eed22
to
1c4fa11
Compare
Missed this review request, sorry! Will tackle this today |
You didn’t miss this! I added you yesterday. I’ve been fishing for a reviewer who is not buried under other priorities! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would need to know more about compartment-mapper to review properly, but here's some questions that occurred to me.
async () => { | ||
await application.import({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
async () => { | |
await application.import({ | |
() => application.import({ |
Just curious, any reason not to do it this way?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that would be a valid simplification.
}, | ||
); | ||
|
||
const application = await parseArchive(archive, 'app.agar', { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could the bad hash fail in parseArchive
? Seems strange to have to put together my globals and other settings to fail later on the hash being bad. I don't know much about how this works, but it seems like it'd be nice to fail faster.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What makes the hash bad isn’t so much that it’s inaccurate as that it doesn’t match the one given to the loader in the later steps.
All of the functions that accept a sha512 power could do a trivial check, like computeSha512(new Uint8Array(0))
and verify that it returns the correct SHA-512 for that or a smattering of known byte sequences. Then it would be possible to fail on parseArchive
, but, on the other hand, then it wouldn’t be possible to create an invalid archive with parseArchive
and test the loader’s ability to recognize that it is invalid.
const { computeSha512 } = readPowers; | ||
|
||
const application = await parseArchive(archive, 'app.agar', { | ||
computeSha512, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, so the desire to check the hash is communicated solely by passing in the capability to check the hash? If I were auditing this, it seems like it is not as clear as it could be, but I guess otherwise you'd have to pass a setting and the authority.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Aye. The alternative is to have a redundant option. I’m not entirely sure which is better.
68f9289
to
383b914
Compare
1c4fa11
to
2a96277
Compare
383b914
to
b9bed57
Compare
2a96277
to
a48da5f
Compare
b9bed57
to
6fb41a8
Compare
a48da5f
to
285a29a
Compare
d2c100f
to
65f2986
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall approach sounds good, but I'd like to make the API as safe to use as possible.
packages/compartment-mapper/NEWS.md
Outdated
every file is consistent. | ||
- `importArchive`, `loadArchive`, and `parseArchive` all optionally accept a | ||
`computeSha512` capability, use it to verify the integrity of the archive | ||
and provide the hash of the contained `compartment-map.json` so the caller |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Two concerns:
- Do the load/execute functions return the computed hash? I think that's too late: the wrong code has already had a chance to execute with whatever authorities you passed in. A safer API would be to pass the expected hash in, have the
compartment-map.json
loader hash what it loads, assert that it matches the expected value, and only proceed if it matches. "No evaluation of unverified code". At each point we're about to load a file, we should know the expected hash ahead of time, and we don't convert the inert bytes into behavior until after it's passed the check. - It sounds like the contents of the compartment map determines whether or not a hash is checked on each module. That sounds reasonable, but we must make sure that this doesn't enable an attacker to substitute a doctored map (lacking module hashes) to disable any checks. I think the hash check of the map itself is sufficient to prevent this (they can't remove the module hashes and still meet the expected map hash), but "in-band" security markers that have broken many others systems (the JWT
"alg": "none"
field comes to mind).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- This is a good catch and I’m revising the
importArchive
,loadArchive
, andparseArchive
functions to acceptexpectedSha512
which will get checked against the archivedcompartment-map.json
. - If you pass
computeSha512
, every module must have asha512
and pass an integrity check, regardless of whether you also provideexpectedSha512
.
let sha512; | ||
if (computeSha512 !== undefined) { | ||
sha512 = computeSha512(transformedBytes); | ||
if (expectedSha512 !== undefined && sha512 !== expectedSha512) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is the expected hash optional here, when the earlier check (in makeImportHook
) makes it mandatory (both in the case where the ability to hash at all has been provided)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is also a good catch. The expectedSha512 is in fact always undefined because this import hook maker only gets used when creating a compartment map from scratch, so we don’t need the code above to extract expectedSha512 (never exists) nor the code below to test it, just this bit in the middle to compute a new one if the power is provided.
Compartment, | ||
}), | ||
{ | ||
message: /failed a SHA-512 integrity check/, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it possible to test a hash mismatch in both the top-level compartment map, and in one of the modules being loaded, separately? Maybe by making the doctored hasher insert the corruption on the first call but not the later ones (and knowing exactly how many time the hasher is called during assembly). That's probably easier than creating a valid archive, taking it apart, flipping a bit in a hash, then reassembling it.
b370ead
to
b138baa
Compare
@warner Addressed feedback in b138baa:
|
); | ||
} | ||
} else { | ||
throw new Error( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I'm reading this correctly, if you 1: provide computeSha512
into the archive importer and 2: the compartment map did not specify a hash for the module, then the import will fail. I'm all for things that encourage hashes, including throwing random errors if you choose to omit them, but this smells of an inconsistency.
- Providing the expected top-level compartment-map hash is a great signal to say that you care about the integrity of the thing you're loading.
- Providing that hash without also providing the means to compute hashes is a clear mistake.
- If you don't care about integrity, you wouldn't provide an expected compartment-map hash, but you might still provide the
computeSha512
tool, just like you'd unconditionally provide the file-reading tool.
- An archive which cares about integrity will include module hashes.
- .. but maybe we're providing for archives which don't care about integrity, and lack those module hashes?
- An archive whose compartment-map does not match the expected hash is a clear error.
So I see three questions:
- If you provide
computeSha512
, but the archive does not include module hashes, I think this switch might fail.. should the application author (who may or may not have provided an expected compartment-map hash) think "oh, this archive should care about integrity more", or should they think "I shouldn't providecomputeSha512
" ? - What should an application which cares about integrity do when asked to load an archive which does not?
- Should we even support the idea of an archive which does not care about integrity?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I like best:
- If you provide
computeSha512
, regardless of whether you provideexpectedSha512
, every module incompartment-map.json
that provides a hash will be checked. Hashes are not required. - If you provide
computeSha512
andexpectedSha512
, the integrity of thecompartment-map.json
hash will be checked. - If you provide
computeSha512
but do not provideexpectedSha512
, the hash ofcompartment-map.json
will not be checked. - If you do not provide
computeSha512
and do provideexpectedSha512
, this is a usage error. - If you provide neither
expectedSha512
norcomputeSha512
, no hashes will be checked, regardless of whether the compartment map has any hashes.
@@ -101,6 +144,20 @@ export const parseArchive = async (archiveBytes, archiveLocation) => { | |||
// TODO validate compartmentMap instead of leaning hard on the above type | |||
// assertion. | |||
|
|||
if (expectedSha512 !== undefined) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it possible to move the hash-equals-expected test earlier, just after we get compartmentMapBytes
and before we do anything else with it? That would be the most hygenic.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure. We can move the check before parsing the content of the compartment map.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Small changes suggested, but if you didn't do them I'd still approve for landing.
b138baa
to
f714c25
Compare
Fixes #794
See change to NEWS.md for details.