diff --git a/.github/workflows/push_image.yml b/.github/workflows/push_image.yml new file mode 100644 index 00000000..8d58299c --- /dev/null +++ b/.github/workflows/push_image.yml @@ -0,0 +1,44 @@ +name: push_image + +on: + push: + branches: + - main + release: + types: [published, created, edited] + +jobs: + build-and-push: + runs-on: ubuntu-latest + steps: + - name: Checkout DIDKit repository + uses: actions/checkout@v2 + with: + path: didkit + + - name: Checkout SSI library + uses: actions/checkout@v2 + with: + repository: spruceid/ssi + token: ${{ secrets.GH_ACCESS_TOKEN_CEL }} + path: didkit/ssi + + - name: Build and push CLI + uses: elgohr/Publish-Docker-Github-Action@master + with: + name: spruceid/didkit-cli + username: ${{ github.actor }} + password: ${{ secrets.SBIHEL_GH_PACKAGE_PUSH_TOKEN }} + registry: ghcr.io + dockerfile: Dockerfile-cli + workdir: didkit + + - name: Build and push HTTP + uses: elgohr/Publish-Docker-Github-Action@master + with: + name: spruceid/didkit-http + username: ${{ github.actor }} + password: ${{ secrets.SBIHEL_GH_PACKAGE_PUSH_TOKEN }} + registry: ghcr.io + dockerfile: Dockerfile-http + workdir: didkit diff --git a/Dockerfile-cli b/Dockerfile-cli new file mode 100644 index 00000000..cba98914 --- /dev/null +++ b/Dockerfile-cli @@ -0,0 +1,14 @@ +FROM ekidd/rust-musl-builder as builder + +ADD --chown=rust:rust . /didkit +ADD --chown=rust:rust ./ssi /ssi +WORKDIR /didkit/cli + +ENV CARGO_NET_GIT_FETCH_WITH_CLI=true +RUN cargo build --release + +FROM alpine +COPY --from=builder /didkit/target/x86_64-unknown-linux-musl/release/didkit didkit +ENTRYPOINT ["./didkit"] + + diff --git a/Dockerfile-http b/Dockerfile-http new file mode 100644 index 00000000..75ab8e39 --- /dev/null +++ b/Dockerfile-http @@ -0,0 +1,13 @@ +FROM ekidd/rust-musl-builder as builder + +ADD --chown=rust:rust . /didkit +ADD --chown=rust:rust ./ssi /ssi +WORKDIR /didkit/http + +ENV CARGO_NET_GIT_FETCH_WITH_CLI=true +RUN cargo build --release + +FROM alpine +COPY --from=builder /didkit/target/x86_64-unknown-linux-musl/release/didkit-http /usr/local/bin/ + +ENTRYPOINT ["didkit-http"] diff --git a/README.md b/README.md index 3c33a995..6b5d074a 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,27 @@ ## Install +### Container + +Both the CLI and HTTP server are containerised and available under +`ghcr.io/spruceid/didkit-(cli|http)`. + +The image are private for now, so a [Personal Access Token](https://docs.github.com/en/free-pro-team@latest/github/authenticating-to-github/creating-a-personal-access-token) +is required. Once created you can login like so: +```bash +$ docker login ghcr.io -u USERNAME --password-stdin +``` + +You can use the images like CLIs: +```bash +$ docker run ghcr.io/spruceid/didkit-cli:latest --help +$ docker run --init -p 8080 ghcr.io/spruceid/didkit-http:latest --port 8080 +``` + +> You can pass JWKs either by sharing a volume with `docker run --volume`, or by passing the JWK directly with `docker run -e JWK=$MY_JWK` or `docker run didkit-http --jwk $MY_JWK`. + +### Manual + DIDKit is written in [Rust][]. To get Rust, you can use [Rustup][]. We depend on some Rust nightly features. When installing with Rustup, pick the nightly release channel. Or run `rustup default nightly` to switch to it. diff --git a/cli/README.md b/cli/README.md index e78bb578..89419547 100644 --- a/cli/README.md +++ b/cli/README.md @@ -35,7 +35,8 @@ Given a Ed25519 [JWK][], output the corresponding [did:key][] [verificationMetho #### Options -- `-k, --key ` (required) - Name of JWK file +- `-k, --key-path ` (required, conflicts with jwk) - Filename of JWK file +- `-j, --jwk ` (required, conflicts with key-path) - JWK. ### `didkit vc-issue-credential` @@ -47,13 +48,14 @@ The proof type is set automatically based on the key file provided. JWK paramete #### Options -Options besides `--key` correspond to linked data [proof options][] as specified in [ld-proofs][] and [vc-http-api][]. +Options besides `--key-path` correspond to linked data [proof options][] as specified in [ld-proofs][] and [vc-http-api][]. - `-C, --challenge ` - [challenge][] property of the proof - `-c, --created ` - [created][] property of the proof. ISO8601 datetime. Defaults to the current time. time. - `-d, --domain ` - [domain][] property of the proof -- `-k, --key ` (required) - Filename of JWK for signing. +- `-k, --key-path ` (required, conflicts with jwk) - Filename of JWK for signing. +- `-j, --jwk ` (required, conflicts with key-path) - JWK for signing. - `-p, --proof-purpose ` [proofPurpose][] property of the proof. - `-v, --verification-method ` [verificationMethod][] property of the proof. URI for proof verification information, e.g. a public key identifier. diff --git a/cli/src/main.rs b/cli/src/main.rs index 56d366c6..51b3ebe9 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -3,7 +3,7 @@ use std::io::{stdin, stdout, BufReader, BufWriter}; use std::path::PathBuf; use chrono::prelude::*; -use structopt::StructOpt; +use structopt::{clap::ArgGroup, StructOpt}; use didkit::{ LinkedDataProofOptions, ProofPurpose, VerifiableCredential, VerifiablePresentation, JWK, @@ -15,13 +15,13 @@ pub enum DIDKit { GenerateEd25519Key, /// Output a did:key DID for a JWK KeyToDIDKey { - #[structopt(short, long, parse(from_os_str))] - key: PathBuf, + #[structopt(flatten)] + key: KeyArg, }, /// Output a verificationMethod for a JWK KeyToVerificationMethod { - #[structopt(short, long, parse(from_os_str))] - key: PathBuf, + #[structopt(flatten)] + key: KeyArg, }, /* @@ -44,8 +44,8 @@ pub enum DIDKit { // VC Functionality /// Issue Credential VCIssueCredential { - #[structopt(short, long, parse(from_os_str))] - key: PathBuf, + #[structopt(flatten)] + key: KeyArg, #[structopt(flatten)] proof_options: ProofOptions, }, @@ -56,8 +56,8 @@ pub enum DIDKit { }, /// Issue Presentation VCIssuePresentation { - #[structopt(short, long, parse(from_os_str))] - key: PathBuf, + #[structopt(flatten)] + key: KeyArg, #[structopt(flatten)] proof_options: ProofOptions, }, @@ -84,18 +84,48 @@ pub enum DIDKit { #[derive(StructOpt, Debug)] pub struct ProofOptions { - #[structopt(short, long)] + #[structopt(env, short, long)] pub verification_method: Option, - #[structopt(short, long)] + #[structopt(env, short, long)] pub proof_purpose: Option, - #[structopt(short, long)] + #[structopt(env, short, long)] pub created: Option>, - #[structopt(short = "C", long)] + #[structopt(env, short = "C", long)] pub challenge: Option, - #[structopt(short, long)] + #[structopt(env, short, long)] pub domain: Option, } +#[derive(StructOpt, Debug)] +#[structopt(group = ArgGroup::with_name("key_group").required(true))] +pub struct KeyArg { + #[structopt(env, short, long, parse(from_os_str), group = "key_group")] + key_path: Option, + #[structopt( + env, + short, + long, + parse(try_from_str = serde_json::from_str), + conflicts_with = "key_path", + group = "key_group", + help = "WARNING: you should not use this through the CLI in a production environment, prefer its environment variable." + )] + jwk: Option, +} + +impl KeyArg { + fn get_jwk(&self) -> JWK { + match &self.key_path { + Some(p) => { + let key_file = File::open(p).unwrap(); + let key_reader = BufReader::new(key_file); + serde_json::from_reader(key_reader).unwrap() + } + None => self.jwk.clone().unwrap(), + } + } +} + impl From for LinkedDataProofOptions { fn from(options: ProofOptions) -> LinkedDataProofOptions { LinkedDataProofOptions { @@ -109,13 +139,6 @@ impl From for LinkedDataProofOptions { } } -fn read_key(key_path: PathBuf) -> JWK { - let key_file = File::open(key_path).unwrap(); - let key_reader = BufReader::new(key_file); - let key: JWK = serde_json::from_reader(key_reader).unwrap(); - key -} - fn main() { let opt = DIDKit::from_args(); match opt { @@ -125,23 +148,18 @@ fn main() { println!("{}", jwk_str); } - DIDKit::KeyToDIDKey { key: key_path } => { - let key = read_key(key_path); - let did = key.to_did().unwrap(); + DIDKit::KeyToDIDKey { key } => { + let did = key.get_jwk().to_did().unwrap(); println!("{}", did); } - DIDKit::KeyToVerificationMethod { key: key_path } => { - let key = read_key(key_path); - let did = key.to_verification_method().unwrap(); + DIDKit::KeyToVerificationMethod { key } => { + let did = key.get_jwk().to_verification_method().unwrap(); println!("{}", did); } - DIDKit::VCIssueCredential { - key: key_path, - proof_options, - } => { - let key: JWK = read_key(key_path); + DIDKit::VCIssueCredential { key, proof_options } => { + let key: JWK = key.get_jwk(); let credential_reader = BufReader::new(stdin()); let mut credential: VerifiableCredential = serde_json::from_reader(credential_reader).unwrap(); @@ -166,11 +184,8 @@ fn main() { } } - DIDKit::VCIssuePresentation { - key: key_path, - proof_options, - } => { - let key: JWK = read_key(key_path); + DIDKit::VCIssuePresentation { key, proof_options } => { + let key: JWK = key.get_jwk(); let presentation_reader = BufReader::new(stdin()); let mut presentation: VerifiablePresentation = serde_json::from_reader(presentation_reader).unwrap(); diff --git a/cli/tests/cli.rs b/cli/tests/cli.rs index d2322342..9c107495 100644 --- a/cli/tests/cli.rs +++ b/cli/tests/cli.rs @@ -15,7 +15,7 @@ fn generate_key() { fn issue_verify_credential_presentation() { // Get DID for key let did_output = Command::new(BIN) - .args(&["key-to-did-key", "-k", "tests/ed25519-key.jwk"]) + .args(&["key-to-did-key", "--key-path", "tests/ed25519-key.jwk"]) .stderr(Stdio::inherit()) .output() .unwrap(); diff --git a/http/README.md b/http/README.md index 28d4e85b..23e76f60 100644 --- a/http/README.md +++ b/http/README.md @@ -26,10 +26,11 @@ and runs until interrupted. - `-p, --port ` - Port to listen on. Default is a random OS-chosen port. - `-k, --key ` - Filename of a JWK to use for issuing credentials and presentations. +- `-j, --jwk ` - JWK to use for issuing credentials and presentations. #### Issuer keys -Provide issuer keys using the `-k`/`--key` option. If none are provided, issuance functionality will be unavailable. If one is provided, that one will be used to sign all credentials and presentations, regardless of the proof options in the issuance request. If more than one key is provided, the issuance request may identify which key to use for signing by its DID in the `verificationMethod` property of the proof options; if none is identified in that property, the first key is used. +Provide issuer keys using the `-k`/`--key-path` or `-j`/`--jwk` options. If none are provided, issuance functionality will be unavailable. If one is provided, that one will be used to sign all credentials and presentations, regardless of the proof options in the issuance request. If more than one key is provided, the issuance request may identify which key to use for signing by its DID in the `verificationMethod` property of the proof options; if none is identified in that property, the first key is used. ## Rust library diff --git a/http/src/main.rs b/http/src/main.rs index 0db96190..137b6af9 100644 --- a/http/src/main.rs +++ b/http/src/main.rs @@ -12,32 +12,53 @@ use didkit_http::Error; #[derive(StructOpt, Debug)] pub struct DIDKitHttpOpts { /// Port to listen on - #[structopt(short, long)] + #[structopt(env, short, long)] port: Option, /// Hostname to listen on - #[structopt(short = "s", long)] + #[structopt(env, short = "s", long)] host: Option, /// JWK to use for issuing - #[structopt(short, long, parse(from_os_str))] - key: Option>, + #[structopt(flatten)] + key: KeyArg, } -fn read_key(path: &PathBuf) -> JWK { - let file = File::open(path).unwrap(); - let reader = BufReader::new(file); - serde_json::from_reader(reader).unwrap() +#[derive(StructOpt, Debug)] +pub struct KeyArg { + #[structopt(env, short, long, parse(from_os_str), group = "key_group")] + key_path: Option>, + #[structopt( + env, + short, + long, + parse(try_from_str = serde_json::from_str), + conflicts_with = "key_path", + group = "key_group", + help = "WARNING: you should not use this through the CLI in a production environment, prefer its environment variable." + )] + jwk: Option>, +} + +impl KeyArg { + fn get_jwks(&self) -> Vec { + match self.key_path.clone() { + Some(paths) => paths + .iter() + .map(|filename| { + let key_file = File::open(filename).unwrap(); + let key_reader = BufReader::new(key_file); + serde_json::from_reader(key_reader).unwrap() + }) + .collect(), + None => self.jwk.clone().unwrap_or_default(), + } + } } #[tokio::main] async fn main() -> Result<(), Error> { let opt = DIDKitHttpOpts::from_args(); - let keys = opt - .key - .unwrap_or_default() - .iter() - .map(|filename| read_key(filename)) - .collect(); + let keys = opt.key.get_jwks(); let makesvc = DIDKitHTTPMakeSvc::new(keys); let host = opt.host.unwrap_or([127, 0, 0, 1].into()); let addr = (host, opt.port.unwrap_or(0)).into();