Skip to content

Commit

Permalink
x509 Verification after VC verification.
Browse files Browse the repository at this point in the history
  • Loading branch information
brianorwhatever committed Jan 10, 2024
1 parent 6027363 commit 3ca6853
Show file tree
Hide file tree
Showing 13 changed files with 718 additions and 27 deletions.
18 changes: 9 additions & 9 deletions common/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {ConfidentialClientApplication} from '@azure/msal-node';
import {didResolver} from './documentLoader.js';
import {generateId} from 'bnid';
import {httpClient} from '@digitalbazaar/http-client';
import {verifyJWKx509} from './x509.js';

// General Utilities

Expand Down Expand Up @@ -78,27 +79,25 @@ export const normalizeVpTokenDataIntegrity = vpToken => {

const verifyJWTVC = async (jwt, options) => {
try {
const vc = await verifyCredentialJWT(jwt, {
const verification = await verifyCredentialJWT(jwt, {
resolve: did => didResolver.get({did})
}, options);
if(vc) {
return {verified: true, vc};
if(verification) {
return {...verification, errors: []};
}
return {verified: false};
} catch(e) {
return {verified: false, errors: [e.message]};
}
};

const verifyJWTVP = async (jwt, options) => {
try {
const vp = await verifyPresentationJWT(jwt, {
const verification = await verifyPresentationJWT(jwt, {
resolve: did => didResolver.get({did})
}, options);
if(vp) {
return {verified: true, vp};
if(verification) {
return {...verification, errors: []};
}
return {verified: false};
} catch(e) {
return {verified: false, errors: [e.message]};
}
Expand All @@ -108,7 +107,8 @@ export const verifyUtils = {
verifyPresentationDataIntegrity: async args => verify(args),
verifyCredentialDataIntegrity: async args => verifyCredential(args),
verifyPresentationJWT: async (jwt, options) => verifyJWTVP(jwt, options),
verifyCredentialJWT: async (jwt, options) => verifyJWTVC(jwt, options)
verifyCredentialJWT: async (jwt, options) => verifyJWTVC(jwt, options),
verifyx509JWT: async jwk => verifyJWKx509(jwk)
};

// MSAL Client Utilities
Expand Down
183 changes: 183 additions & 0 deletions common/x509.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
import * as x509 from '@peculiar/x509';
import {Certificate, CertificateRevocationList} from 'pkijs';
import {createPublicKey, X509Certificate} from 'node:crypto';
import {config} from '../config/config.js';
import ocsp from 'ocsp';

const checkDates = cert => {
const now = new Date();
const validFrom = new Date(cert.validFrom);
const validTo = new Date(cert.validTo);
if(now < validFrom || now > validTo) {
return {verified: false, errors: [
'Certificate is not valid at the current time'
]};
}
return {verified: true, errors: []};
};

const checkKeyUsage = cert => {
const pec = new x509.X509Certificate(cert.toString());
const keyUsages = pec.getExtension('2.5.29.15'); // key usage extension
const digitalSignatureBit = 0;

// Check to make sure key usage includes digital signatures
if((keyUsages.usages & (1 << digitalSignatureBit)) === 0) {
return {verified: false, errors: [
`Certificate doesn't have digital signature key usage`
]};
}
return {verified: true, errors: []};
};

const checkRevocation = async (cert, issuer) => {
const errors = [];
if(typeof cert.infoAccess !== 'undefined') {
await new Promise(resolve => {
ocsp.check({
cert: cert.raw,
issuer: issuer.raw
}, function(err, res) {
if(err !== null || res.type !== 'good') {
errors.push('x509 certificate has been revoked (OCSP)');
}
resolve();
});
});
}

/*
* TEMPORARILY DISABLED
const certificate = Certificate.fromBER(cert.raw);
const ext = certificate.extensions.find(ext => ext.extnID === '2.5.29.31');
if(ext) {
const crlURIs = new Set([]);
for(const points of ext.parsedValue.distributionPoints) {
for(const point of points.distributionPoint) {
crlURIs.add(point.value);
}
}
for(const uri of crlURIs) {
const resp = await fetch(uri);
if(resp.status === 200) {
const crlBER = await resp.arrayBuffer();
const crl = CertificateRevocationList.fromBER(crlBER);
const revoked = crl.isCertificateRevoked(Certificate.fromBER(cert.raw));
if(revoked) {
errors.push(`x509 certificate has been revoked (CRL)`);
}
} else {
errors.push(`Failed to query CRL at ${uri} - Received ${resp.status}`);
}
}
}
*/
return {verified: errors.length === 0, errors};
};

const checkSignature = async (cert, parentCert) => {
const errors = [];

// verify signature
const verified = cert.verify(parentCert.publicKey);
if(!verified) {
errors.push(`X509 certificate invalid`);
}

return {verified: errors.length === 0, errors};
};

const checkTrust = async certs => {
let errors = [];
let i = 0;
for(const cert of certs) {
if(i < certs.length - 1) {
const issued = cert.checkIssued(certs[i + 1]);
if(!issued) {
errors.push(`X509 certificate at index ${i} not issued by parent.`);
} else {
const verified = await checkSignature(cert, certs[i + 1]);
if(!verified.verified) {
errors = errors.concat(verified.errors);
}
const revocation = await checkRevocation(cert, certs[i + 1]);
if(!revocation.verified) {
errors = errors.concat(revocation.errors);
}
}
} else {
// Issuer in CA Store
let found = false;
for(const caCertRaw of config.caStore) {
const caCert = new X509Certificate(caCertRaw);
if(certs[i].checkIssued(caCert)) {
found = true;
const verified = await checkSignature(certs[i], caCert);
if(!verified.verified) {
errors = errors.concat(verified.errors);
}
const revocation = await checkRevocation(certs[i], caCert);
if(!revocation.verified) {
errors = errors.concat(revocation.errors);
}
break;
}
}
if(!found) {
errors.push(
`Issuer of X509 certificate at index ${i} not found in CA store`);
}
}
i++;
}
return {verified: errors.length === 0, errors};
};

export const verifyX509 = async certs => {
try {
let errors = [];
for(const cert of certs) {
// Verify Expiration Date
const datesVerify = checkDates(cert);
if(!datesVerify.verified) {
errors = errors.concat(datesVerify.errors);
}

// Check Key Usage
const keyUsageVerify = checkKeyUsage(cert);
if(!keyUsageVerify.verified) {
errors = errors.concat(keyUsageVerify.errors);
}
}

// Check Trust
const trustVerify = await checkTrust(certs);
if(!trustVerify.verified) {
errors = errors.concat(trustVerify.errors);
}
return {verified: errors.length === 0, errors};
} catch(e) {
console.error(e);
return {verified: false, errors: [e.message]};
}
};

export const verifyJWKx509 = async jwk => {
try {
const certs = jwk.x5c.map(x5c =>
new X509Certificate(Buffer.from(x5c, 'base64')));

// Verify public key matches certificate
const key = createPublicKey({key: jwk, format: 'jwk'});
if(!certs[0].publicKey.equals(key)) {
return {
verified: false,
errors: ['Public key is not found in leaf certificate']
};
}
return await verifyX509(certs);
} catch(e) {
console.error(e);
return {verified: false, errors: [e.message]};
}
};
7 changes: 7 additions & 0 deletions config/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,12 @@ const relyingParties = configRPs.map(rp => {
};
});

/**
* A list of trusted root certificates
*/
const caStore = (configDoc.caStore ?? [])
.map(cert => cert.pem);

export const config = {
databaseConnectionUri,
didWeb,
Expand All @@ -275,4 +281,5 @@ export const config = {
relyingParties,
signingKeys,
translations,
caStore
};
21 changes: 17 additions & 4 deletions controllers/exchanges/native.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,12 +103,25 @@ export const verifySubmission = async (vp_token, submission, exchange) => {
errors = errors.concat(vpResult.errors);
}
const vc = jp.query(
vpResult.vp.verifiablePresentation,
vpResult.verifiablePresentation,
submitted.path_nested.path
)[0];
const result = await verifyUtils.verifyCredentialJWT(vc.proof.jwt);
if(!result.verified) {
errors = errors.concat(result.errors);
if(vc) {
const result = await verifyUtils.verifyCredentialJWT(vc?.proof?.jwt);
if(!result.verified) {
errors = errors.concat(result.errors);
} else {
if(result.signer.publicKeyJwk?.x5c) {
const certValid = await verifyUtils.verifyx509JWT(
result.signer.publicKeyJwk
);
if(!certValid.verified) {
errors = errors.concat(certValid.errors);
}
}
}
} else {
errors = errors.concat('VC not found in presentation');
}
} else if(submitted.format === 'ldp_vp') {
vp = normalizeVpTokenDataIntegrity(vp_token)[0];
Expand Down
7 changes: 7 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@
"@digitalbazaar/vdl-context": "github:digitalbazaar/vdl-context#context-update-10-24-2023",
"@digitalbazaar/x25519-key-agreement-key-2020": "^3.0.1",
"@interop/did-web-resolver": "^3.0.1",
"@peculiar/x509": "^1.9.5",
"asn1js": "^3.0.5",
"base64url": "^3.0.1",
"bnid": "^3.0.0",
"cors": "^2.8.5",
Expand All @@ -56,6 +58,10 @@
"jsonpath": "^1.1.1",
"mongodb": "^6.2.0",
"morgan": "^1.10.0",
"node-forge": "^1.3.1",
"ocsp": "^1.2.0",
"oidc-provider": "^8.4.0",
"pkijs": "^3.0.15",
"qrcode": "^1.5.3",
"quasar": "^2.13.0",
"swagger-jsdoc": "^6.2.8",
Expand All @@ -64,6 +70,7 @@
"x25519-key-agreement-2020-context": "^1.0.0"
},
"devDependencies": {
"@peculiar/webcrypto": "^1.4.3",
"@vitejs/plugin-vue": "^4.2.3",
"autoprefixer": "^10.4.16",
"eslint": "^8.49.0",
Expand Down
7 changes: 4 additions & 3 deletions tests/api.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -220,11 +220,12 @@ describe('OpenCred API - Native Workflow', function() {
exchange_jwt
);
const verifyUtilsStub = sinon.stub(verifyUtils, 'verifyPresentationJWT')
.resolves({verified: true, vp: {
.resolves({
verified: true,
verifiablePresentation: {vc: {proof: {jwt: '...'}}}}
});
);
const verifyUtilsStub2 = sinon.stub(verifyUtils, 'verifyCredentialJWT')
.resolves({verified: true});
.resolves({verified: true, signer: {}});
const updateStub = sinon.stub(exchanges, 'updateOne').resolves();
const response = await request(app)
.post(`/workflows/${testRP.workflow.id}/exchanges/${exchange.id}/` +
Expand Down
31 changes: 31 additions & 0 deletions tests/fixtures/expired.badssl.com.cer
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
-----BEGIN CERTIFICATE-----
MIIFSzCCBDOgAwIBAgIQSueVSfqavj8QDxekeOFpCTANBgkqhkiG9w0BAQsFADCB
kDELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G
A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxNjA0BgNV
BAMTLUNPTU9ETyBSU0EgRG9tYWluIFZhbGlkYXRpb24gU2VjdXJlIFNlcnZlciBD
QTAeFw0xNTA0MDkwMDAwMDBaFw0xNTA0MTIyMzU5NTlaMFkxITAfBgNVBAsTGERv
bWFpbiBDb250cm9sIFZhbGlkYXRlZDEdMBsGA1UECxMUUG9zaXRpdmVTU0wgV2ls
ZGNhcmQxFTATBgNVBAMUDCouYmFkc3NsLmNvbTCCASIwDQYJKoZIhvcNAQEBBQAD
ggEPADCCAQoCggEBAMIE7PiM7gTCs9hQ1XBYzJMY61yoaEmwIrX5lZ6xKyx2PmzA
S2BMTOqytMAPgLaw+XLJhgL5XEFdEyt/ccRLvOmULlA3pmccYYz2QULFRtMWhyef
dOsKnRFSJiFzbIRMeVXk0WvoBj1IFVKtsyjbqv9u/2CVSndrOfEk0TG23U3AxPxT
uW1CrbV8/q71FdIzSOciccfCFHpsKOo3St/qbLVytH5aohbcabFXRNsKEqveww9H
dFxBIuGa+RuT5q0iBikusbpJHAwnnqP7i/dAcgCskgjZjFeEU4EFy+b+a1SYQCeF
xxC7c3DvaRhBB0VVfPlkPz0sw6l865MaTIbRyoUCAwEAAaOCAdUwggHRMB8GA1Ud
IwQYMBaAFJCvajqUWgvYkOoSVnPfQ7Q6KNrnMB0GA1UdDgQWBBSd7sF7gQs6R2lx
GH0RN5O8pRs/+zAOBgNVHQ8BAf8EBAMCBaAwDAYDVR0TAQH/BAIwADAdBgNVHSUE
FjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwTwYDVR0gBEgwRjA6BgsrBgEEAbIxAQIC
BzArMCkGCCsGAQUFBwIBFh1odHRwczovL3NlY3VyZS5jb21vZG8uY29tL0NQUzAI
BgZngQwBAgEwVAYDVR0fBE0wSzBJoEegRYZDaHR0cDovL2NybC5jb21vZG9jYS5j
b20vQ09NT0RPUlNBRG9tYWluVmFsaWRhdGlvblNlY3VyZVNlcnZlckNBLmNybDCB
hQYIKwYBBQUHAQEEeTB3ME8GCCsGAQUFBzAChkNodHRwOi8vY3J0LmNvbW9kb2Nh
LmNvbS9DT01PRE9SU0FEb21haW5WYWxpZGF0aW9uU2VjdXJlU2VydmVyQ0EuY3J0
MCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5jb21vZG9jYS5jb20wIwYDVR0RBBww
GoIMKi5iYWRzc2wuY29tggpiYWRzc2wuY29tMA0GCSqGSIb3DQEBCwUAA4IBAQBq
evHa/wMHcnjFZqFPRkMOXxQhjHUa6zbgH6QQFezaMyV8O7UKxwE4PSf9WNnM6i1p
OXy+l+8L1gtY54x/v7NMHfO3kICmNnwUW+wHLQI+G1tjWxWrAPofOxkt3+IjEBEH
fnJ/4r+3ABuYLyw/zoWaJ4wQIghBK4o+gk783SHGVnRwpDTysUCeK1iiWQ8dSO/r
ET7BSp68ZVVtxqPv1dSWzfGuJ/ekVxQ8lEEFeouhN0fX9X3c+s5vMaKwjOrMEpsi
8TRwz311SotoKQwe6Zaoz7ASH1wq7mcvf71z81oBIgxw+s1F73hczg36TuHvzmWf
RwxPuzZEaFZcVlmtqoq8
-----END CERTIFICATE-----
31 changes: 31 additions & 0 deletions tests/fixtures/revoked-cert/intermediate.cer
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
-----BEGIN CERTIFICATE-----
MIIFUTCCBDmgAwIBAgIQB5g2A63jmQghnKAMJ7yKbDANBgkqhkiG9w0BAQsFADBh
MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD
QTAeFw0yMDA3MTYxMjI1MjdaFw0yMzA1MzEyMzU5NTlaMFkxCzAJBgNVBAYTAlVT
MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxMzAxBgNVBAMTKlJhcGlkU1NMIFRMUyBE
ViBSU0EgTWl4ZWQgU0hBMjU2IDIwMjAgQ0EtMTCCASIwDQYJKoZIhvcNAQEBBQAD
ggEPADCCAQoCggEBANpuQ1VVmXvZlaJmxGVYotAMFzoApohbJAeNpzN+49LbgkrM
Lv2tblII8H43vN7UFumxV7lJdPwLP22qa0sV9cwCr6QZoGEobda+4pufG0aSfHQC
QhulaqKpPcYYOPjTwgqJA84AFYj8l/IeQ8n01VyCurMIHA478ts2G6GGtEx0ucnE
fV2QHUL64EC2yh7ybboo5v8nFWV4lx/xcfxoxkFTVnAIRgHrH2vUdOiV9slOix3z
5KPs2rK2bbach8Sh5GSkgp2HRoS/my0tCq1vjyLJeP0aNwPd3rk5O8LiffLev9j+
UKZo0tt0VvTLkdGmSN4h1mVY6DnGfOwp1C5SK0MCAwEAAaOCAgswggIHMB0GA1Ud
DgQWBBSkjeW+fHnkcCNtLik0rSNY3PUxfzAfBgNVHSMEGDAWgBQD3lA1VtFMu2bw
o+IbG8OXsj3RVTAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0lBBYwFAYIKwYBBQUHAwEG
CCsGAQUFBwMCMBIGA1UdEwEB/wQIMAYBAf8CAQAwNAYIKwYBBQUHAQEEKDAmMCQG
CCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wewYDVR0fBHQwcjA3
oDWgM4YxaHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0R2xvYmFsUm9v
dENBLmNybDA3oDWgM4YxaHR0cDovL2NybDQuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0
R2xvYmFsUm9vdENBLmNybDCBzgYDVR0gBIHGMIHDMIHABgRVHSAAMIG3MCgGCCsG
AQUFBwIBFhxodHRwczovL3d3dy5kaWdpY2VydC5jb20vQ1BTMIGKBggrBgEFBQcC
AjB+DHxBbnkgdXNlIG9mIHRoaXMgQ2VydGlmaWNhdGUgY29uc3RpdHV0ZXMgYWNj
ZXB0YW5jZSBvZiB0aGUgUmVseWluZyBQYXJ0eSBBZ3JlZW1lbnQgbG9jYXRlZCBh
dCBodHRwczovL3d3dy5kaWdpY2VydC5jb20vcnBhLXVhMA0GCSqGSIb3DQEBCwUA
A4IBAQAi49xtSOuOygBycy50quCThG45xIdUAsQCaXFVRa9asPaB/jLINXJL3qV9
J0Gh2bZM0k4yOMeAMZ57smP6JkcJihhOFlfQa18aljd+xNc6b+GX6oFcCHGr+gsE
yPM8qvlKGxc5T5eHVzV6jpjpyzl6VEKpaxH6gdGVpQVgjkOR9yY9XAUlFnzlOCpq
sm7r2ZUKpDfrhUnVzX2nSM15XSj48rVBBAnGJWkLPijlACd3sWFMVUiKRz1C5PZy
el2l7J/W4d99KFLSYgoy5GDmARpwLc//fXfkr40nMY8ibCmxCsjXQTe0fJbtrrLL
yWQlk9VDV296EI/kQOJNLVEkJ54P
-----END CERTIFICATE-----
Loading

0 comments on commit 3ca6853

Please sign in to comment.