Skip to content
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

verify: init #311

Merged
merged 29 commits into from
Apr 17, 2024
Merged

verify: init #311

merged 29 commits into from
Apr 17, 2024

Conversation

jleightcap
Copy link
Contributor

@jleightcap jleightcap commented Nov 16, 2023

Summary

Complementary half of #310, enable the verification of bundles:

$ cargo run --manifest-path tests/conformance/Cargo.toml -- sign-bundle --bundle bundle.txt.sigstore --identity-token $(sigstore-python get-identity-token) bundle.txt
# ...
Operation succeeded!
$ cargo run --manifest-path tests/conformance/Cargo.toml -- verify-bundle --bundle bundle.txt.sigstore --certificate-identity [email protected] --certificate-oidc-issuer https://github.com/login/oauth bundle.txt
# ...
Operation succeeded!

Release Note

Documentation

@jleightcap jleightcap mentioned this pull request Nov 16, 2023
@jleightcap jleightcap force-pushed the jl/verify branch 2 times, most recently from 3246492 to b3dc1ea Compare December 6, 2023 18:36
flavio added a commit that referenced this pull request Dec 21, 2023
Add ability to sign artifacts à la sigstore-python, towards Bundle signing and verification.
Follow-up from #305, and the prerequisite motivation to #311.

Signed-off-by: Jack Leightcap <[email protected]>
Signed-off-by: Andrew Pan <[email protected]>
Signed-off-by: Andrew Pan <[email protected]>
Signed-off-by: Jack Leightcap <[email protected]>
Co-authored-by: Andrew Pan <[email protected]>
Co-authored-by: Andrew Pan <[email protected]>
Co-authored-by: Andrew Pan <[email protected]>
Co-authored-by: Flavio Castelli <[email protected]>
@jleightcap jleightcap force-pushed the jl/verify branch 5 times, most recently from dfecc13 to 5c1792a Compare January 5, 2024 22:23
src/crypto/certificate.rs Show resolved Hide resolved
src/crypto/certificate.rs Show resolved Hide resolved
}

// A certificate that is its own issuer and signer is considered a root CA.
// TODO(jl): verify_directly_issued_by
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


if inclusion_proof.is_some() && !has_checkpoint {
// TODO(tnytown): Act here.
// NOTE(jl): in sigstore-python, this is a no-op that prints a warning log.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure how best to flag a warning of invalid state in this case, AFAIK there isn't a logging framework to hook into given this is a library crate.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think we depend on tracing, so we could potentially use that. a good follow-on task might be to go around the new bundle code and add traces + define a reasonable tracing span hierarchy.

src/verify/models.rs Outdated Show resolved Hide resolved
@jleightcap jleightcap marked this pull request as ready for review January 5, 2024 22:36
Copy link
Contributor

@tnytown tnytown left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for fixing this up! i have a few comments and clarifications :)

Comment on lines 1 to 8
use rustls_pki_types::CertificateDer;
use webpki::TrustAnchor;

/// Machinery for Sigstore end entity certificate verification.
struct CertificateVerificationContext<'a> {
pub trust_anchors: Vec<TrustAnchor<'a>>,
pub intermediate_certs: Vec<CertificateDer<'a>>,
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is chaff from an older iteration of the API, we can probably remove it!

src/verify/models.rs Outdated Show resolved Hide resolved

if inclusion_proof.is_some() && !has_checkpoint {
// TODO(tnytown): Act here.
// NOTE(jl): in sigstore-python, this is a no-op that prints a warning log.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think we depend on tracing, so we could potentially use that. a good follow-on task might be to go around the new bundle code and add traces + define a reasonable tracing span hierarchy.

@@ -23,3 +23,4 @@ macro_rules! tuf_resource {
}

pub(crate) const SIGSTORE_ROOT: &[u8] = tuf_resource!("prod/root.json");
pub(crate) const _SIGSTORE_TRUST_BUNDLE: &[u8] = tuf_resource!("prod/trusted_root.json");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we may want to investigate taking advantage of this in the tuf module

Comment on lines 122 to 133
let (_, extended_key_usage_ext): (bool, ExtendedKeyUsage) = materials
.certificate
.tbs_certificate
.get()
.expect("Malformed certificate")
.expect("Malformed certificate");

if !extended_key_usage_ext.0.contains(&ID_KP_CODE_SIGNING) {
return Err(VerificationError::CertificateTypeError(
"Extended key usage does not contain `code signing`".into(),
));
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is already covered by store.verify_cert_with_time above, so I think it can be removed outright.

Comment on lines 107 to 120
// TODO(tnytown): How likely is a malformed certificate in this position? Do we want to
// account for it and create an error type as opposed to unwrapping?
let (_, key_usage_ext): (bool, KeyUsage) = materials
.certificate
.tbs_certificate
.get()
.expect("Malformed certificate")
.expect("Malformed certificate");

if !key_usage_ext.digital_signature() {
return Err(VerificationError::CertificateTypeError(
"Key usage is not of type `digital signature`".into(),
));
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check should be unnecessary, since chain building (above) should assert that the leaf contains a keyUsage.digitalSignature.

Comment on lines 86 to 97
let rekor_entry = if let Some(rekor_entry) = rekor_entry {
let cell = OnceCell::new();

// TODO(tnytown): Switch to setting if offline when Rekor fetching is implemented.
#[allow(clippy::unwrap_used)]
cell.set(rekor_entry).unwrap();

cell
} else {
Default::default()
};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm having a little trouble following this: why do we put LogEntry into a OnceCell, rather than just keeping it as Option<LogEntry> with a None variant when we're missing an entry?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no good reason, I think I originally used OnceCell to allow for interior mutability when we lazy-init the entry; I don't think that's ever done in practice (and we shouldn't need to cache the online entry anyways.)

Comment on lines 22 to 31
macro_rules! required {
($($base:expr )? ; $first_attr:ident $( . $rest_attrs:ident)* $( , $else_err:expr)?) => {
$( $base . )? $first_attr.as_ref()
$(
.and_then(|v| v.$rest_attrs.as_ref())
)*
$( .ok_or($else_err) )?
}
}
pub(crate) use required;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO this macro's usages are pretty hard to read -- I think we'd possibly be better off unfolding these inline (or using more bindings to minimize the number of error sites), plus implementing TryInto or another conversion trait where appropriate.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this wouldn't be necessary with better protobuf codegen. we can put this PR on hold until we figure that out

Signed-off-by: Jack Leightcap <[email protected]>
Signed-off-by: Jack Leightcap <[email protected]>
@jleightcap
Copy link
Contributor Author

@flavio I'll continue to flush out documentation and cleanup this afternoon, but this should be feature complete. let me know if there's anything I can do to help facilitate review here.

@flavio
Copy link
Member

flavio commented Feb 1, 2024

@jleightcap, I skimmed through the PR and I've the following questions:

  • There's at least a todo()! invocation. I think we cannot merge that code until the actual implementation is provided
  • What about all the TODO(s)? Should we address them before merging it?
  • Can you look into the failed checks please?
  • I'm probably saying something stupid, but... isn't this verification code redoing some of the checks done by sigstore::cosign? What are the plans for the new verify module?

Copy link

@jasperpatterson jasperpatterson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was playing around with these changes as I'm excited to see this functionality merged and noticed a couple little issues in my testing.

return None;
}

for chain_cert in chain_certs {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the chain_certs be added to the VerificationMaterials? It appears they are thrown away right now and chain verification does not currently work if intermediates are present.

Copy link
Contributor

@tnytown tnytown Feb 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIRC, intermediates should never be included in materials produced against the public-good instance; if they are present, the spec says we should ignore them. Fulcio's intermediates, if any, are distributed via TUF. For non-PGI verifications, we would want to plumb chain_certs through.

log_index: required!(tlog_entry; log_index)?.parse().ok()?,
verification: Verification {
inclusion_proof: parsed_inclusion_proof,
signed_entry_timestamp: required!(; inclusion_promise.signed_entry_timestamp)?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct! I think the newer revisions I've pushed up handle this correctly.

Some((false, _)) => {
// BasicConstraints must be marked as critical, per RFC 5280 4.2.1.9.
return Err(SigstoreError::InvalidCertError(
"invalid X.509 certificate: non-critical BasicConstraints in CA".to_string(),
Copy link

@jasperpatterson jasperpatterson Feb 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In is_root_ca, you're assuming an Ok result from this function, to determine whether something isn't a CA cert:

if !is_ca(certificate)? {
    return Ok(false);
}

However this situation is valid for an end certificate, and not an error, meaning that current logic doesn't quite work:

https://www.rfc-editor.org/rfc/rfc5280#section-4.2.1.9

This extension MAY appear as a critical or non-critical extension in end entity certificates.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch!

}

for chain_cert in chain_certs {
if is_root_ca(chain_cert).is_ok() {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you need to specifically check the bool here, not just for Ok.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, you're right. I don't know how I feel about the Result<bool> return type, it feels easy to misuse. Maybe it'd be better to switch to bool and collapse the error states into false.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed is_ca, is_leaf, and is_root_ca to return Result<(), _>.

Copy link
Contributor

@tnytown tnytown left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Programmatic note: we @trailofbits are out of time on sigstore-rs. If this is not reviewed and merged by the end of the month, we will have to drop this changeset. I've fixed up what I can this cycle, but we don't have capacity for too many further review cycles.


/// Verify the signature provided has been actually generated by the given key
/// when signing the provided prehashed message.
pub fn verify_prehash(&self, signature: Signature, msg: &[u8]) -> Result<()> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've changed the visibility to pub(crate) for now.

src/errors.rs Outdated
@@ -109,6 +109,9 @@ pub enum SigstoreError {
#[error("Certificate pool error: {0}")]
CertificatePoolError(String),

#[error("Certificate invalid: {0}")]
InvalidCertError(String),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed :)

serde_json::from_slice(&buf).map_err(serde::de::Error::custom)
let buf: Vec<u8> = Base64::<Standard, Padded>::deserialize_as(de)?;

// The first two bytes indicate the signature and hash algorithms so let's skip those.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not a huge deal since we are getting this info from the trustroot CTFE keys. We could certainly assert on an expected value here if you'd prefer :)


// The first two bytes indicate the signature and hash algorithms so let's skip those.
// The next two bytes indicate the size of the signature.
let signature_size = u16::from_be_bytes(buf[2..4].try_into().expect("unexpected length"));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIRC usize::from_be_bytes accepts exactly 4 bytes

src/lib.rs Outdated
@@ -285,8 +285,13 @@ pub mod rekor;
pub mod tuf;

// Don't export yet -- these types should only be useful internally.
#[cfg(feature = "bundle")]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was supposed to gate our sigstore_protobuf_specs dep, but apparently I didn't do so. I've updated it now :)

src/tuf/mod.rs Outdated
pub fn prefetch(self) -> Result<Self> {
let _ = self.trusted_root()?;
Ok(self)
pub fn new(checkout_dir: Option<&Path>) -> Result<Self> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair point, updated :)

src/tuf/mod.rs Outdated
Ok(Self { trusted_root })
}

fn fetch_target<N>(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noted, interesting food for thought :) I think we'd need to put some further thought into supporting those kinds of environments (a pure AsyncRead + AsyncWrite wouldn't work because we support multiple targets.)

src/tuf/mod.rs Outdated
let local_path = checkout_dir.as_ref().map(|d| d.join(name.raw()));

// Try reading the target from disk cache.
let data = if let Some(Ok(local_data)) = local_path.as_ref().map(std::fs::read) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, we wouldn't be able to naively return the error here: the file potentially doesn't exist in the cache yet.

src/tuf/mod.rs Outdated
// Write the up-to-date data back to the disk. This doesn't need to succeed, as we can
// always fetch the target again later.
if let Some(local_path) = local_path {
let _ = std::fs::write(local_path, &data);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!


// Parse raw string without DER encoding.
let val = std::str::from_utf8(ext.extn_value.as_bytes())
.expect("failed to parse constructed Extension!");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice catch! I didn't realize that this was user input.

Signed-off-by: Andrew Pan <[email protected]>
@tnytown
Copy link
Contributor

tnytown commented Apr 10, 2024

CC @flavio, this is ready for review :)

Copy link
Member

@flavio flavio left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, I left some comments.

Aside from my comments, it would be great to see the other comments addressed. For TODO stuff/nodes the outcome of the conversation can be added into the source code as a comment

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm happy about the changes, it's also great to see the TUF contents are now synchronized with the local checkout.

Can you please add some unit tests to cover that? The repository_helper file (removed by this PR) had some tests you might be able to reuse

Copy link
Contributor

@tnytown tnytown Apr 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

71f5161

I switched to using the online TUF repo in tests and deleted the copy we had checked in.

/// A synchronous Sigstore verifier.
///
/// Async callers must use [`AsyncVerifier`]. Async usage of [`Verifier`] will result in a deadlock.
pub struct Verifier {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like @tannaurus' suggestion

tests/conformance/conformance.rs Show resolved Hide resolved
Copy link
Contributor

@tnytown tnytown left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I've addressed all previous comments at this point (I reacted when only action was taken and commented in all other cases); let me know if I've missed any. I don't have permission to dismiss conversations on this PR because I'm not the author.

CC @flavio

/// A synchronous Sigstore verifier.
///
/// Async callers must use [`AsyncVerifier`]. Async usage of [`Verifier`] will result in a deadlock.
pub struct Verifier {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is done: 1e73783

tests/conformance/conformance.rs Show resolved Hide resolved
Copy link
Contributor

@tnytown tnytown Apr 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

71f5161

I switched to using the online TUF repo in tests and deleted the copy we had checked in.

Copy link
Member

@flavio flavio left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left a minor comment, I'm fine with this PR.

);
}

macro_rules! impl_test {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand why you're using this macro, but it makes the tests less readable. You can achieve the same result using the rstest crate, which is already a dev dependency.

Finally, you can use the tokio::test macro to decorate the unit test, which removes more boilerplate from the test.

I can do the changes afterwards, I understand this PR has been ongoing since a long time

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@flavio
Copy link
Member

flavio commented Apr 12, 2024

@tannaurus: can you take one last look at the PR before I merge it?

Copy link
Member

@flavio flavio left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we're finally ready to merge it. Sorry for the long time it took!

@flavio flavio merged commit d5279eb into sigstore:main Apr 17, 2024
6 checks passed
@tannaurus
Copy link
Contributor

I'm so happy to see this merged! Thanks for all the work @tnytown and @jleightcap ! ❤️ 🚀

Sorry for my delayed response here. I was out of town for team offsite and then came down with a bug when I got back.

@woodruffw woodruffw deleted the jl/verify branch April 18, 2024 17:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants