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

did:x509 issuer support in IETF profile #206

Merged
merged 31 commits into from
Aug 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
1d7e3ad
Minimal demo of JS eval
eddyashton Jul 23, 2024
688c453
Confirming function access, return value handling
eddyashton Jul 23, 2024
a56c5f0
Convert COSE protected headers to a JSON object, pass to exported JS …
eddyashton Jul 24, 2024
3209756
Load policy script from configuration
eddyashton Jul 24, 2024
bcd64f7
Convert to PEM before inserting into JS
eddyashton Jul 24, 2024
cacbc15
Testing
eddyashton Jul 24, 2024
520b664
Cosntitution validation of new field
eddyashton Jul 24, 2024
32ffc95
Minor cleanups
eddyashton Jul 24, 2024
6f138a9
Remove debug logging
eddyashton Jul 24, 2024
588ef9a
Placate mypy
eddyashton Jul 25, 2024
2957528
Implement PR suggestions: Document config entry, return failure reaso…
eddyashton Jul 25, 2024
048d539
Cleaner debug logging
eddyashton Jul 25, 2024
256496a
Update trivial_false test with new return type
eddyashton Jul 26, 2024
df801e1
Different formatter -_-
eddyashton Jul 26, 2024
6924b88
Merge branch 'main' of https://github.com/microsoft/scitt-ccf-ledger …
eddyashton Jul 26, 2024
2180d5d
Remove redundant breaks
eddyashton Jul 26, 2024
e73a4f6
Update app/src/policy_engine.h
eddyashton Jul 26, 2024
e3453ff
Update test/test_configuration.py
eddyashton Jul 26, 2024
511d2fc
wip
achamayou Jul 30, 2024
bf31fde
wip
achamayou Jul 30, 2024
1970079
more wip
achamayou Aug 2, 2024
bdf2249
Add EKU check to test
achamayou Aug 5, 2024
bb1d36a
DID model mismatch
achamayou Aug 6, 2024
65ca9ce
key comparison
achamayou Aug 6, 2024
da6231d
fmt
achamayou Aug 7, 2024
088a6f0
Merge branch 'main' into didx509
achamayou Aug 7, 2024
f9e8343
Merge branch 'main' into didx509
achamayou Aug 7, 2024
ea80e71
Split out didx509 validation
achamayou Aug 7, 2024
ea968a4
rename inject_eku
achamayou Aug 7, 2024
3b1cc10
Remove stale comment
achamayou Aug 7, 2024
0249632
Expand tests
achamayou Aug 8, 2024
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
50 changes: 50 additions & 0 deletions app/src/did/document.h
Original file line number Diff line number Diff line change
Expand Up @@ -199,3 +199,53 @@ namespace scitt::did
return method.public_key_jwk.value();
}
}

// Alternative DID document spec imported from CCF/src/node/did.h
// Unlike scitt::did::DidDocument, this expects a single string for
// assertion_method and leaves the JWK parsing to specific sub-type to the
// caller based on the kty, rather than expose a single merged type where every
// field is optional. This is needed now for compatibility with didx509cpp, but
// the types should be merged eventually if they are still both needed.
namespace scitt::did::alt
{
// From https://www.w3.org/TR/did-core.
// Note that the types defined in this file do not exhaustively cover
// all fields and types from the spec.
struct DIDDocumentVerificationMethod
{
std::string id;
std::string type;
std::string controller;
std::optional<nlohmann::json> public_key_jwk = std::nullopt;

bool operator==(const DIDDocumentVerificationMethod&) const = default;
};
DECLARE_JSON_TYPE_WITH_OPTIONAL_FIELDS(DIDDocumentVerificationMethod);
DECLARE_JSON_REQUIRED_FIELDS(
DIDDocumentVerificationMethod, id, type, controller);
DECLARE_JSON_OPTIONAL_FIELDS_WITH_RENAMES(
DIDDocumentVerificationMethod, public_key_jwk, "publicKeyJwk");

struct DIDDocument
{
std::string id;
std::string context;
std::string type;
std::vector<DIDDocumentVerificationMethod> verification_method = {};
nlohmann::json assertion_method = {};

bool operator==(const DIDDocument&) const = default;
};
DECLARE_JSON_TYPE_WITH_OPTIONAL_FIELDS(DIDDocument);
DECLARE_JSON_REQUIRED_FIELDS(DIDDocument, id);
DECLARE_JSON_OPTIONAL_FIELDS_WITH_RENAMES(
DIDDocument,
context,
"@context",
type,
"type",
verification_method,
"verificationMethod",
assertion_method,
"assertionMethod");
}
2 changes: 0 additions & 2 deletions app/src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -274,8 +274,6 @@ namespace scitt
SCITT_DEBUG("No policy applied");
}

// TODO: Apply further acceptance policies.

auto service = ctx.tx.template ro<ccf::Service>(ccf::Tables::SERVICE);
auto service_info = service->get().value();
auto service_cert = service_info.cert;
Expand Down
100 changes: 92 additions & 8 deletions app/src/verifier.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

#include "cose.h"
#include "did/resolver.h"
#include "didx509cpp/didx509cpp.h"
#include "openssl_wrappers.h"
#include "profiles.h"
#include "public_key.h"
Expand Down Expand Up @@ -162,6 +163,81 @@ namespace scitt::verifier
return key;
}

void process_ietf_didx509_subprofile(
const cose::ProtectedHeader& phdr, const std::vector<uint8_t>& data)
{
// NB: In later revisions of SCITT, x5chain is unprotected, and
// only x5t is. This logic will need to authenticate x5chain[0]
// against x5t before it can proceed to verify the signature.
// Verify the signature as early as possible
OpenSSL::Unique_X509 leaf = parse_certificate(phdr.x5chain.value()[0]);
PublicKey key(leaf, std::nullopt);
try
{
cose::verify(data, key);
}
catch (const cose::COSESignatureValidationError& e)
{
throw VerificationError(e.what());
}

// Then authenticate the did:x509 claim against the x5chain
std::string pem_chain;
for (auto const& c : phdr.x5chain.value())
{
pem_chain += ccf::crypto::cert_der_to_pem(c).str();
}
auto did_document_str = didx509::resolve(
pem_chain, phdr.issuer.value(), true /* Do not validate time */);
scitt::did::alt::DIDDocument did_document =
nlohmann::json::parse(did_document_str);

if (did_document.verification_method.empty())
{
throw VerificationError(
"Could not find verification method in resolved DID "
"document");
}
// x5chain has a single leaf certificate, so the verification
// method should also have a single key
if (did_document.verification_method.size() != 1)
{
throw VerificationError(
"Unexpected number of verification methods in resolved DID "
"document");
}
auto const& vm = did_document.verification_method[0];
if (vm.controller != phdr.issuer.value())
{
throw VerificationError(
"Verification method controller does not match issuer");
}

if (!vm.public_key_jwk.has_value())
{
throw VerificationError(
"Verification method does not contain a public key");
}

auto resolved_jwk =
vm.public_key_jwk.value().get<ccf::crypto::JsonWebKey>();
// Need to dispatch on kty if we want to support RSA, Eddsa, etc.
if (resolved_jwk.kty != ccf::crypto::JsonWebKeyType::EC)
{
throw VerificationError(
"Verification method public key is not an EC key");
}
auto signing_key =
ccf::crypto::make_verifier(phdr.x5chain.value()[0])->public_key_jwk();

if (resolved_jwk != signing_key)
{
throw VerificationError(
"Resolved verification method public key does not match "
"signing key");
}
}

void validate_notary_protected_header(
const cose::ProtectedHeader& phdr, const Configuration& configuration)
{
Expand Down Expand Up @@ -362,17 +438,25 @@ namespace scitt::verifier
}
else if (phdr.issuer.has_value())
{
// IETF SCITT claim
key = process_ietf_profile(
phdr, tx, current_time, resolution_cache_expiry, configuration);

try
if (phdr.issuer->starts_with("did:x509") && phdr.x5chain.has_value())
{
cose::verify(data, key);
// IETF SCITT did:x509 claim
process_ietf_didx509_subprofile(phdr, data);
}
catch (const cose::COSESignatureValidationError& e)
else
{
throw VerificationError(e.what());
// IETF SCITT did:web claim
key = process_ietf_profile(
phdr, tx, current_time, resolution_cache_expiry, configuration);

try
{
cose::verify(data, key);
}
catch (const cose::COSESignatureValidationError& e)
{
throw VerificationError(e.what());
}
}

profile = ClaimProfile::IETF;
Expand Down
18 changes: 16 additions & 2 deletions pyscitt/pyscitt/crypto.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ def generate_cert(
issuer: Optional[Tuple[Pem, Pem]] = None,
ca: bool = False,
cn: Optional[str] = None,
add_eku: Optional[str] = None,
):
if not cn:
cn = str(uuid4())
Expand Down Expand Up @@ -230,9 +231,13 @@ def generate_cert(
critical=True,
)
.add_extension(x509.BasicConstraints(ca=ca, path_length=None), critical=True)
.sign(issuer_key, hash_alg)
)
return cert.public_bytes(Encoding.PEM).decode("ascii")
if add_eku:
cert = cert.add_extension(
x509.ExtendedKeyUsage([x509.ObjectIdentifier(add_eku)]), critical=False
)
signed_cert = cert.sign(issuer_key, hash_alg)
return signed_cert.public_bytes(Encoding.PEM).decode("ascii")


def get_priv_key_type(priv_pem: str) -> str:
Expand Down Expand Up @@ -276,6 +281,15 @@ def get_cert_fingerprint(pem: Pem) -> str:
return cert.fingerprint(hashes.SHA256()).hex()


def get_cert_fingerprint_b64url(pem: Pem) -> str:
cert = load_pem_x509_certificate(pem.encode("ascii"))
return (
base64.urlsafe_b64encode(cert.fingerprint(hashes.SHA256()))
.decode("ascii")
.strip("=")
)


def get_public_key_fingerprint(pem: Pem) -> str:
pub_key = load_pem_public_key(pem.encode("ascii"))
der = pub_key.public_bytes(Encoding.DER, PublicFormat.SubjectPublicKeyInfo)
Expand Down
10 changes: 10 additions & 0 deletions test/infra/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,16 @@ def trusted_ca(client) -> X5ChainCertificateAuthority:
return ca


@pytest.fixture(scope="class")
def untrusted_ca(client) -> X5ChainCertificateAuthority:
"""
Create a X5ChainCertificateAuthority but do not add its root to the SCITT service.

The service will reject claims signed using certificates issued by the CA.
"""
return X5ChainCertificateAuthority(kty="ec")


@pytest.fixture(scope="class")
def trust_store(client) -> StaticTrustStore:
"""
Expand Down
8 changes: 8 additions & 0 deletions test/infra/x5chain_certificate_authority.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,13 @@ def create_chain(
self, *, length: int = 1, ca: bool = False, **kwargs
) -> Tuple[List[Pem], Pem]:
assert length > 0
generate_cert_kwargs = {}
# Unlike the rest of the kwards, which are handed over to the keypair generation
# call, add_eku is passed to the certificate generation call.
add_eku = "add_eku"
if add_eku in kwargs:
generate_cert_kwargs[add_eku] = kwargs[add_eku]
del kwargs[add_eku]

chain = [(self.root_cert_pem, self.root_key_pem)]
for i in range(length):
Expand All @@ -37,6 +44,7 @@ def create_chain(
private_key_pem=private_key,
issuer=chain[-1],
ca=(i < length - 1) or ca,
**generate_cert_kwargs
)
chain.append((cert_pem, private_key))

Expand Down
Loading
Loading