Skip to content

Commit

Permalink
Add support for issuer custom/alternative origin (#2356)
Browse files Browse the repository at this point in the history
* Add support for issuer custom/alternative origin

This adds frontend support and end-to-end testing for custom issuer
derivation origins. In particular:

* Updates the VC flow to support looking up & validating the derivation
  origin (see spec for details)
* Fixes a couple of bugs in VC issuer canister provisioning
* Adds support for provisioning the VC issuer canister with derivation
  origin used in the end-to-end tests
* Remove frontend checks in the issuer canister (caused issues in the
  end-to-end tests because non-configurable (beyond provisioning) and
  not actually used)
* Add a nice origin for the issuer canister in the vite config
* Update all VC tests to use the `derivation_origin` for issuer
  (somewhat fiddly/brittle due to having a single issuer)

* Update src/frontend/src/test-e2e/verifiableCredentials/alternativeOrigins.test.ts
  • Loading branch information
nmattia authored Mar 12, 2024
1 parent 01a6cd0 commit 2d9f0d7
Show file tree
Hide file tree
Showing 17 changed files with 263 additions and 50 deletions.
4 changes: 1 addition & 3 deletions .github/workflows/canister-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -468,13 +468,11 @@ jobs:

- name: Deploy canisters
run: |
# NOTE: dfx install will run the postinstall scripts from dfx.json
dfx canister install internet_identity --wasm internet_identity_test.wasm.gz
dfx canister install test_app --wasm demos/test-app/test_app.wasm
dfx canister install issuer --wasm demos/vc_issuer/vc_demo_issuer.wasm.gz
- name: Provision issuer canister
run: ./demos/vc_issuer/provision

# Check the return code of npm ci
- name: Check on npm ci
# make sure we don't wait too long if for some unlikely reason the npm status file doesn't get created
Expand Down
10 changes: 10 additions & 0 deletions demos/vc_issuer/app/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,16 @@ const App = () => {
/>
</label>
</section>
<section>
<label>
Custom principal:
<input
data-role="custom-principal"
type="text"
onChange={(evt) => setPrincipal(evt.target.value)}
/>
</label>
</section>
<section>
<button
data-action="authenticate"
Expand Down
6 changes: 3 additions & 3 deletions demos/vc_issuer/provision
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,12 @@ do
shift;
;;
--issuer-derivation-origin)
DERIVATION_ORIGIN="${2:?missing value for '--issuer-derivation-origin'}"
ISSUER_DERIVATION_ORIGIN="${2:?missing value for '--issuer-derivation-origin'}"
shift; # shift past --issuer-canister & value
shift;
;;
--issuer-frontend-hostname)
FRONTEND_HOSTNAME="${2:?missing value for '--issuer-frontend-hostname'}"
ISSUER_FRONTEND_HOSTNAME="${2:?missing value for '--issuer-frontend-hostname'}"
shift; # shift past --issuer-canister & value
shift;
;;
Expand All @@ -84,7 +84,7 @@ done
DFX_NETWORK="${DFX_NETWORK:-local}"
II_CANISTER_ID="${II_CANISTER_ID:-$(dfx canister id internet_identity)}"
ISSUER_CANISTER="${ISSUER_CANISTER:-issuer}"
ISSUER_CANISTER_ID=$(dfx canister id $ISSUER_CANISTER)
ISSUER_CANISTER_ID=$(dfx canister id "$ISSUER_CANISTER")
DEFAULT_DERIVATION_ORIGIN="https://$ISSUER_CANISTER_ID.icp0.io"
ISSUER_DERIVATION_ORIGIN="${ISSUER_DERIVATION_ORIGIN:-$DEFAULT_DERIVATION_ORIGIN}"
ISSUER_FRONTEND_HOSTNAME="${ISSUER_FRONTEND_HOSTNAME:-$ISSUER_DERIVATION_ORIGIN}"
Expand Down
17 changes: 9 additions & 8 deletions demos/vc_issuer/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -307,15 +307,16 @@ async fn derivation_origin(
fn get_derivation_origin(hostname: &str) -> Result<DerivationOriginData, DerivationOriginError> {
CONFIG.with_borrow(|config| {
let config = config.get();
if hostname == config.frontend_hostname {
Ok(DerivationOriginData {
origin: config.derivation_origin.clone(),
})
} else {
Err(DerivationOriginError::UnsupportedOrigin(
hostname.to_string(),
))

// We don't currently rely on the value provided, so if it doesn't match
// we just print a warning
if hostname != config.frontend_hostname {
println!("*** achtung! bad frontend hostname {}", hostname,);
}

Ok(DerivationOriginData {
origin: config.derivation_origin.clone(),
})
})
}

Expand Down
17 changes: 0 additions & 17 deletions demos/vc_issuer/tests/issue_credential.rs
Original file line number Diff line number Diff line change
Expand Up @@ -371,23 +371,6 @@ fn should_return_derivation_origin_with_custom_init() {
assert_eq!(response.origin, custom_init.derivation_origin);
}

#[test]
fn should_fail_derivation_origin_if_unsupported_origin() {
let env = env();
let canister_id = install_canister(&env, VC_ISSUER_WASM.clone());
let req = DerivationOriginRequest {
frontend_hostname: "https://wrong.fe.host".to_string(),
};
let response =
api::derivation_origin(&env, canister_id, principal_1(), &req).expect("API call failed");
assert_eq!(
response,
Err(DerivationOriginError::UnsupportedOrigin(
req.frontend_hostname
))
);
}

fn employee_credential_spec() -> CredentialSpec {
let mut args = HashMap::new();
args.insert(
Expand Down
2 changes: 1 addition & 1 deletion dfx.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"candid": "demos/vc_issuer/vc_demo_issuer.did",
"wasm": "demos/vc_issuer/vc_demo_issuer.wasm.gz",
"build": "demos/vc_issuer/build.sh",
"post_install": "demos/vc_issuer/provision",
"post_install": "bash -c 'demos/vc_issuer/provision --issuer-derivation-origin https://$(dfx canister id test_app).icp0.io'",
"dependencies": [ "internet_identity" ]
}
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
"aborted_internal_error": "There was an unexpected issue while verifying your credentials. Please retry, and if the problem persist contact support.",
"aborted_auth_failed_ii": "There was an error authenticating you with your Internet Identity. Aliases were not created.",
"aborted_auth_failed_issuer": "There was an error authenticating you with the issuer. Make sure you can authenticate with the issuer.",
"aborted_invalid_derivation_origin_issuer": "The derivation origin given for the issuer is not valid.",
"aborted_derivation_origin_issuer_error": "The issuer produced an error when queried for its derivation origin.",
"aborted_issuer_api_error": "There was an error requesting credentials with the issuer. The issuer returned an error.",
"aborted_bad_principal_rp": "The principal provided by the relying party does not match the data Internet Identity has.",
"aborted_no_canister_id": "Internet Identity could not find the canister for the issuer.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ export type AbortReason =
| "internal_error"
| "auth_failed_ii"
| "auth_failed_issuer"
| "derivation_origin_issuer_error"
| "invalid_derivation_origin_issuer"
| "issuer_api_error"
| "bad_principal_rp"
| "no_canister_id"
Expand Down
66 changes: 61 additions & 5 deletions src/frontend/src/flows/verifiableCredentials/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,16 +91,32 @@ const verifyCredentials = async ({
}
}

const validationResult = await withLoader(() =>
// Verify that principals may be issued to RP using the specified
// derivation origin
const validRpDerivationOrigin = await withLoader(() =>
validateDerivationOrigin(rpOrigin_, rpDerivationOrigin)
);
if (validationResult.result === "invalid") {
if (validRpDerivationOrigin.result === "invalid") {
return abortedCredentials({ reason: "bad_derivation_origin_rp" });
}
validationResult.result satisfies "valid";
validRpDerivationOrigin.result satisfies "valid";
const rpOrigin = rpDerivationOrigin ?? rpOrigin_;

const vcIssuer = new VcIssuer(issuerCanisterId);

const issuerDerivationOriginResult = await getValidatedIssuerDerivationOrigin(
{
vcIssuer,
issuerOrigin,
}
);

if (issuerDerivationOriginResult.kind === "error") {
return abortedCredentials({ reason: issuerDerivationOriginResult.err });
}

const issuerDerivationOrigin = issuerDerivationOriginResult.origin;

// XXX: We don't check that the language matches the user's language. We need
// to figure what to do UX-wise first.
const consentInfo = await vcIssuer.getConsentMessage({ credentialSpec });
Expand Down Expand Up @@ -164,7 +180,7 @@ const verifyCredentials = async ({

const pAliasPending = getAliasCredentials({
rpOrigin,
issuerOrigin,
issuerOrigin: issuerDerivationOrigin /* Use the actual derivation origin */,
authenticatedConnection,
});

Expand All @@ -180,7 +196,7 @@ const verifyCredentials = async ({

const issuedCredential = await issueCredential({
vcIssuer,
issuerOrigin,
issuerOrigin: issuerDerivationOrigin,
issuerAliasCredential: pAlias.issuerAliasCredential,
credentialSpec,
authenticatedConnection,
Expand Down Expand Up @@ -302,6 +318,46 @@ const getAliasCredentials = async ({
return { ok: { rpAliasCredential, issuerAliasCredential } };
};

// Looks up and verify the derivation origin to be used by the issuer
const getValidatedIssuerDerivationOrigin = async ({
vcIssuer,
issuerOrigin,
}: {
vcIssuer: VcIssuer;
issuerOrigin: string;
}): Promise<
| {
kind: "error";
err:
| "derivation_origin_issuer_error"
| "invalid_derivation_origin_issuer";
}
| { kind: "ok"; origin: string }
> => {
const result = await vcIssuer.getDerivationOrigin({ origin: issuerOrigin });

if (result.kind === "error") {
return { kind: "error", err: "derivation_origin_issuer_error" };
}

result.kind satisfies "origin";

const validDerivationOrigin = await validateDerivationOrigin(
issuerOrigin,
result.origin
);
if (validDerivationOrigin.result === "invalid") {
console.error(
"Invalid derivation origin for issuer",
JSON.stringify(validDerivationOrigin)
);
return { kind: "error", err: "invalid_derivation_origin_issuer" };
}
validDerivationOrigin.result satisfies "valid";

return { kind: "ok", origin: result.origin };
};

// Contact the issuer to issue the credentials
const issueCredential = async ({
vcIssuer,
Expand Down
32 changes: 31 additions & 1 deletion src/frontend/src/flows/verifiableCredentials/vcIssuer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,9 @@ export class VcIssuer {
});

if ("Err" in result) {
console.error("Could not prepare credential", result.Err);
console.error(
"Could not prepare credential: " + JSON.stringify(result.Err)
);
return "error";
}

Expand Down Expand Up @@ -107,4 +109,32 @@ export class VcIssuer {

return result.Ok;
};

getDerivationOrigin = async ({
origin,
}: {
origin: string;
}): Promise<{ kind: "origin"; origin: string } | { kind: "error" }> => {
const actor = await this.createActor();

let result;
try {
result = await actor.derivation_origin({
frontend_hostname: origin,
});
} catch (e: unknown) {
console.error("Could not get derivation origin (unexpected error)", e);
return { kind: "error" };
}

if ("Err" in result) {
console.error(
"Could not get derivation origin (issuer error)",
JSON.stringify(result.Err)
);
return { kind: "error" };
}

return { kind: "origin", origin: result.Ok.origin };
};
}
3 changes: 3 additions & 0 deletions src/frontend/src/test-e2e/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ export const TEST_APP_NICE_URL = "https://nice-name.com";
export const ISSUER_APP_URL = `https://${issuerAppCanisterId}.icp0.io`;
export const ISSUER_APP_URL_LEGACY = `https://${issuerAppCanisterId}.ic0.app`;

// Value needs to match how the canister was provisioned
export const ISSUER_CUSTOM_ORIGIN_NICE_URL = `https://nice-issuer-custom-orig.com`;

export const II_URL =
process.env.II_URL ?? "https://identity.internetcomputer.org";

Expand Down
Loading

0 comments on commit 2d9f0d7

Please sign in to comment.