├── .githooks └── pre-commit ├── .github ├── actions │ └── setup │ │ └── action.yml └── workflows │ ├── ci.yml │ └── writecache.yml ├── .gitignore ├── .gitmodules ├── .golangci.yaml ├── .vscode └── settings.json ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── GOVERNANCE.md ├── Justfile ├── LICENSE ├── README.md ├── WEB5.code-workspace ├── bin ├── .go-1.22.0.pkg ├── .golangci-lint-1.56.2.pkg ├── .just-1.23.0.pkg ├── README.hermit.md ├── activate-hermit ├── go ├── gofmt ├── golangci-lint ├── hermit ├── hermit.hcl └── just ├── cmd └── web5 │ ├── README.md │ ├── cmd_did_create.go │ ├── cmd_did_resolve.go │ ├── cmd_jwt_decode.go │ ├── cmd_jwt_sign.go │ ├── cmd_jwt_verify.go │ ├── cmd_vc_create.go │ ├── cmd_vc_sign.go │ ├── cmd_vcjwt_decode.go │ ├── cmd_vcjwt_verify.go │ └── main.go ├── crypto ├── README.md ├── doc.go ├── dsa │ ├── dsa.go │ ├── dsa_test.go │ ├── ecdsa │ │ ├── ecdsa.go │ │ ├── secp256k1.go │ │ └── secp256k1_test.go │ └── eddsa │ │ ├── ed25519.go │ │ ├── ed25519_test.go │ │ └── eddsa.go ├── entropy.go ├── entropy_test.go ├── keymanager.go └── keymanager_test.go ├── diagrams └── dids-pkg.png ├── dids ├── README.md ├── did │ ├── bearerdid.go │ ├── bearerdid_test.go │ ├── did.go │ ├── did_test.go │ └── portabledid.go ├── didcore │ ├── document.go │ ├── document_test.go │ └── resolution.go ├── diddht │ ├── diddht.go │ ├── diddht_test.go │ ├── internal │ │ ├── bencode │ │ │ ├── bencode.go │ │ │ └── bencode_test.go │ │ ├── bep44 │ │ │ ├── bep44.go │ │ │ └── bep44_test.go │ │ ├── dns │ │ │ ├── constants.go │ │ │ ├── did.go │ │ │ ├── did_test.go │ │ │ ├── dns.go │ │ │ ├── dns_test.go │ │ │ └── dnsmappings.go │ │ └── pkarr │ │ │ ├── gateway.go │ │ │ └── gateway_test.go │ ├── resolver.go │ └── resolver_test.go ├── didjwk │ ├── didjwk.go │ └── didjwk_test.go ├── didweb │ ├── didweb.go │ └── didweb_test.go └── resolver.go ├── go.mod ├── go.sum ├── jwk ├── jwk.go └── jwk_test.go ├── jws ├── README.md ├── jws.go └── jws_test.go ├── jwt ├── README.md ├── jwt.go └── jwt_test.go ├── pexv2 ├── pd.go ├── pexv2.go └── pexv2_test.go ├── scripts └── web5 ├── testvectors.go └── vc ├── examples_test.go ├── vc.go ├── vc_test.go ├── vcjwt.go └── vcjwt_test.go /.githooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Move to the git repository root 4 | REPO_ROOT=$(git rev-parse --show-toplevel) 5 | cd "$REPO_ROOT" 6 | 7 | # Stash non-staged changes to avoid linting them 8 | git stash push -q --keep-index 9 | trap 'git stash pop -q' EXIT INT TERM 10 | 11 | # Run golangci-lint on all staged Go files 12 | echo "Running golangci-lint on staged Go files..." 13 | STAGED_GO_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep '\.go$') 14 | if [ -n "$STAGED_GO_FILES" ]; then 15 | # Use xargs to run golangci-lint on each file 16 | golangci-lint run --new-from-rev=HEAD~ 17 | fi 18 | 19 | exit 0 20 | 21 | -------------------------------------------------------------------------------- /.github/actions/setup/action.yml: -------------------------------------------------------------------------------- 1 | name: Setup build environment 2 | description: Setup build environment 3 | runs: 4 | using: "composite" 5 | steps: 6 | - uses: cashapp/activate-hermit@v1 7 | with: 8 | cache: true 9 | - id: find-go-build-cache 10 | shell: bash 11 | run: echo "cache=$(go env GOCACHE)" >> $GITHUB_OUTPUT 12 | - uses: actions/cache/restore@v4 13 | with: 14 | path: | 15 | ~/go/pkg/mod 16 | ${{ steps.find-go-build-cache.outputs.cache }} 17 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 18 | restore-keys: | 19 | ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 20 | ${{ runner.os }}-go- 21 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | pull_request: 6 | name: CI 7 | concurrency: 8 | group: ${{ github.ref }}-ci 9 | cancel-in-progress: true 10 | jobs: 11 | build: 12 | name: Build 13 | timeout-minutes: 10 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: ./.github/actions/setup 18 | - run: just build 19 | test: 20 | name: Test 21 | timeout-minutes: 10 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/checkout@v4 25 | with: 26 | # Fetches all history for all tags and branches 27 | fetch-depth: 0 28 | # Initializes submodules recursively 29 | submodules: recursive 30 | - uses: ./.github/actions/setup 31 | - run: just test 32 | - name: Upload coverage reports to Codecov 33 | uses: codecov/codecov-action@v4.0.1 34 | with: 35 | token: ${{ secrets.CODECOV_TOKEN }} 36 | slug: decentralized-identity/web5-go 37 | lint: 38 | name: Lint 39 | timeout-minutes: 10 40 | runs-on: ubuntu-latest 41 | steps: 42 | - uses: actions/checkout@v4 43 | - uses: ./.github/actions/setup 44 | - run: just lint 45 | -------------------------------------------------------------------------------- /.github/workflows/writecache.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | workflow_dispatch: 6 | concurrency: 7 | group: ${{ github.ref }}-writecache 8 | cancel-in-progress: true 9 | name: Write Cache 10 | jobs: 11 | write-cache: 12 | name: Write Cache 13 | timeout-minutes: 10 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: ./.github/actions/setup 18 | - run: just build 19 | - id: find-go-build-cache 20 | shell: bash 21 | run: echo "cache=$(go env GOCACHE)" >> $GITHUB_OUTPUT 22 | - uses: actions/cache/save@v4 23 | with: 24 | path: | 25 | ~/go/pkg/mod 26 | ${{ steps.find-go-build-cache.outputs.cache }} 27 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .hermit 2 | .DS_Store 3 | target/ 4 | build/ 5 | deploy/ 6 | dist/ -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "web5-spec"] 2 | path = web5-spec 3 | url = git@github.com:decentralized-identity/web5-spec.git 4 | -------------------------------------------------------------------------------- /.golangci.yaml: -------------------------------------------------------------------------------- 1 | run: 2 | tests: true 3 | timeout: 5m 4 | 5 | output: 6 | print-issued-lines: false 7 | 8 | linters: 9 | enable-all: true 10 | disable: 11 | - maligned 12 | - megacheck 13 | - lll 14 | - typecheck # `go build` catches this, and it doesn't currently work with Go 1.11 modules 15 | - goimports # horrendously slow with go modules :( 16 | - dupl # has never been actually useful 17 | - gochecknoglobals 18 | - gochecknoinits 19 | - interfacer # author deprecated it because it provides bad suggestions 20 | - funlen 21 | - whitespace 22 | - godox 23 | - wsl 24 | - dogsled 25 | - gomnd 26 | - gocognit 27 | - gocyclo 28 | - scopelint 29 | - godot 30 | - nestif 31 | - testpackage 32 | - goerr113 33 | - gci 34 | - gofumpt 35 | - exhaustivestruct 36 | - nlreturn 37 | - forbidigo 38 | - cyclop 39 | - paralleltest 40 | - ifshort # so annoying 41 | - golint 42 | - tagliatelle 43 | - gomoddirectives 44 | - varnamelen 45 | - ireturn 46 | - containedctx 47 | - nilnil 48 | - contextcheck 49 | - nonamedreturns 50 | - exhaustruct 51 | - nosnakecase 52 | - nosprintfhostport 53 | - nilerr 54 | - goconst 55 | - prealloc 56 | - deadcode # doesn't support generics 57 | - varcheck # doesn't support generics 58 | - structcheck # doesn't support generics 59 | - rowserrcheck # doesn't support generics 60 | - wastedassign # doesn't support generics 61 | - goprintffuncname 62 | - dupword 63 | - errchkjson 64 | - musttag 65 | - gofmt # autofmt 66 | - interfacebloat 67 | - tagalign 68 | - nolintlint 69 | - wrapcheck # We might want to re-enable this if we manually wrap all the existing errors with fmt.Errorf 70 | - testableexamples 71 | 72 | 73 | linters-settings: 74 | exhaustive: 75 | default-signifies-exhaustive: true 76 | dupl: 77 | threshold: 100 78 | goconst: 79 | min-len: 8 80 | min-occurrences: 3 81 | gocyclo: 82 | min-complexity: 20 83 | gocritic: 84 | disabled-checks: 85 | - ifElseChain 86 | depguard: 87 | rules: 88 | main: 89 | deny: 90 | - pkg: github.com/pkg/errors 91 | desc: "use fmt.Errorf or errors.New" 92 | - pkg: github.com/alecthomas/errors 93 | desc: "use fmt.Errorf or errors.New" 94 | - pkg: braces.dev/errtrace 95 | desc: "use fmt.Errorf or errors.New" 96 | gosec: 97 | excludes: 98 | - G601 99 | 100 | issues: 101 | max-same-issues: 0 102 | max-issues-per-linter: 0 103 | exclude-use-default: false 104 | exclude: 105 | # Captured by errcheck. 106 | - "^(G104|G204):" 107 | # Very commonly not checked. 108 | - 'Error return value of .(.*\.Help|.*\.MarkFlagRequired|(os\.)?std(out|err)\..*|.*Close|.*Flush|os\.Remove(All)?|.*Print(f|ln|)|os\.(Un)?Setenv). is not checked' 109 | # Weird error only seen on Kochiku... 110 | - "internal error: no range for" 111 | - 'exported method `.*\.(MarshalJSON|UnmarshalJSON|URN|Payload|GoString|Close|Provides|Requires|ExcludeFromHash|MarshalText|UnmarshalText|Description|Check|Poll|Severity)` should have comment or be unexported' 112 | - "composite literal uses unkeyed fields" 113 | - 'declaration of "err" shadows declaration' 114 | - "by other packages, and that stutters" 115 | - "Potential file inclusion via variable" 116 | - "at least one file in a package should have a package comment" 117 | - "bad syntax for struct tag pair" 118 | - "package-comments" 119 | - "parameter testing.TB should have name tb" 120 | - "blank-imports" 121 | - 'should have comment \(or a comment on this block\) or be unexported' 122 | - caseOrder 123 | - unused-parameter -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "go.lintTool":"golangci-lint", 3 | "go.lintFlags": [ 4 | "--fast" 5 | ], 6 | "go.lintOnSave": "package" 7 | } -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # This CODEOWNERS file denotes the project leads 2 | # and encodes their responsibilities for code review. 3 | 4 | # Instructions: At a minimum, replace the '@GITHUB_USER_NAME_GOES_HERE' 5 | # here with at least one project lead. 6 | 7 | # Lines starting with '#' are comments. 8 | # Each line is a file pattern followed by one or more owners. 9 | # The format is described: https://github.blog/2017-07-06-introducing-code-owners/ 10 | 11 | # These owners will be the default owners for everything in the repo. 12 | 13 | * @alecthomas @KendallWeihe @mihai-chiorean @mistermoe @tomdaffurn @wesbillman 14 | 15 | # ----------------------------------------------- 16 | # BELOW THIS LINE ARE TEMPLATES, UNUSED 17 | # ----------------------------------------------- 18 | # Order is important. The last matching pattern has the most precedence. 19 | # So if a pull request only touches javascript files, only these owners 20 | # will be requested to review. 21 | # *.js @octocat @github/js 22 | 23 | # You can also use email addresses if you prefer. 24 | # docs/* docs@example.com -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 2 | # TBD Code of Conduct 3 | 4 | TBD builds infrastructure for the next wave of innovation in financial services — which we believe will be decentralized, permissionless, and non-custodial. This means opening the global economy to everyone. We extend the same principles of inclusion to our developer ecosystem. We are excited to build with you. So we will ensure our community is truly open, transparent and inclusive. Because of the global nature of our project, diversity and inclusivity is paramount to our success. We not only welcome diverse perspectives, we **need** them! 5 | 6 | The code of conduct below reflects the expectations for ourselves and for our community. 7 | 8 | 9 | ## Our Pledge 10 | 11 | We as members, contributors, and leaders pledge to make participation in our 12 | community a harassment-free experience for everyone, regardless of age, physical appearance, visible or invisible disability, ethnicity, sex characteristics, gender 13 | identity and expression, level of experience, education, socio-economic status, 14 | nationality, personal appearance, race, caste, color, religion, or sexual 15 | identity and orientation. 16 | 17 | We pledge to act and interact in ways that contribute to an open, welcoming, 18 | diverse, inclusive, and healthy community. 19 | 20 | ## Our Standards 21 | 22 | Examples of behavior that contributes to a positive environment for our 23 | community include: 24 | 25 | * Demonstrating empathy and kindness toward other people 26 | * Being respectful and welcoming of differing opinions, viewpoints, and experiences 27 | * Giving and gracefully accepting constructive feedback 28 | * Accepting responsibility and apologizing to those affected by our mistakes, 29 | and learning from the experience 30 | * Focusing on what is best not just for us as individuals, but for the overall 31 | community 32 | 33 | Examples of unacceptable behavior include: 34 | 35 | * The use of sexualized language or imagery, and sexual attention or advances of 36 | any kind 37 | * Trolling, insulting or derogatory comments, and personal or political attacks 38 | * Public or private harassment 39 | * Publishing others' private information, such as a physical or email address, 40 | without their explicit permission 41 | * Other conduct which could reasonably be considered inappropriate in a 42 | professional setting 43 | 44 | ## Enforcement Responsibilities 45 | 46 | The TBD Open Source Governance Committee (GC) is responsible for clarifying and enforcing our standards of 47 | acceptable behavior and will take appropriate and fair corrective action in 48 | response to any behavior that they deem inappropriate, threatening, offensive, 49 | or harmful. 50 | 51 | The GC has the right and responsibility to remove, edit, or reject 52 | comments, commits, code, wiki edits, issues, and other contributions that are 53 | not aligned to this Code of Conduct, and will communicate reasons for moderation 54 | decisions when appropriate. 55 | 56 | ## Scope 57 | 58 | This Code of Conduct applies within all project spaces, and it also applies when an individual is representing the project or its community in public spaces. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event, or any space where the project is listed as part of your profile. 59 | 60 | ## Enforcement 61 | 62 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 63 | reported to the TBD Open Source Governance Committee (GC) at 64 | `tbd-open-source-governance@squareup.com`. 65 | All complaints will be reviewed and investigated promptly and fairly. 66 | 67 | The GC is obligated to respect the privacy and security of the 68 | reporter of any incident. 69 | 70 | ## Enforcement Guidelines 71 | 72 | The GC will follow these Community Impact Guidelines in determining 73 | the consequences for any action they deem in violation of this Code of Conduct: 74 | 75 | ### 1. Correction 76 | 77 | **Community Impact**: Use of inappropriate language or other behavior deemed 78 | unprofessional or unwelcome in the community. 79 | 80 | **Consequence**: A private, written warning from the GC, providing 81 | clarity around the nature of the violation and an explanation of why the 82 | behavior was inappropriate. A public apology may be requested. 83 | 84 | ### 2. Warning 85 | 86 | **Community Impact**: A violation through a single incident or series of 87 | actions. 88 | 89 | **Consequence**: A warning with consequences for continued behavior. No 90 | interaction with the people involved, including unsolicited interaction with 91 | those enforcing the Code of Conduct, for a specified period of time. This 92 | includes avoiding interactions in community spaces as well as external channels 93 | like social media and forums. 94 | 95 | Although this list cannot be exhaustive, we explicitly honor diversity in age, culture, ethnicity, gender identity or expression, language, national origin, political beliefs, profession, race, religion, sexual orientation, socioeconomic status, and technical ability. We will not tolerate discrimination based on any of the protected characteristics above, including participants with disabilities. 96 | 97 | Violating these terms may lead to a temporary or permanent ban. 98 | 99 | ### 3. Temporary Ban 100 | 101 | **Community Impact**: A serious violation of community standards, including 102 | sustained inappropriate behavior. 103 | 104 | **Consequence**: A temporary ban from any sort of interaction or public 105 | communication with the community for a specified period of time. No public or 106 | private interaction with the people involved, including unsolicited interaction 107 | with those enforcing the Code of Conduct, is allowed during this period. 108 | Violating these terms may lead to a permanent ban. 109 | 110 | ### 4. Permanent Ban 111 | 112 | **Community Impact**: Demonstrating a pattern of violation of community 113 | standards, including sustained inappropriate behavior, harassment of an 114 | individual, or aggression toward or disparagement of classes of individuals. 115 | 116 | **Consequence**: A permanent ban from any sort of public interaction within the 117 | community. 118 | 119 | ## Attribution 120 | 121 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 122 | version 2.1, available at 123 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. 124 | 125 | Community Impact Guidelines were inspired by 126 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 127 | 128 | For answers to common questions about this code of conduct, see the FAQ at 129 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at 130 | [https://www.contributor-covenant.org/translations][translations]. 131 | 132 | [homepage]: https://www.contributor-covenant.org 133 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html 134 | [Mozilla CoC]: https://github.com/mozilla/diversity 135 | [FAQ]: https://www.contributor-covenant.org/faq 136 | [translations]: https://www.contributor-covenant.org/translations 137 | -------------------------------------------------------------------------------- /GOVERNANCE.md: -------------------------------------------------------------------------------- 1 | # TBD Open Source Project Governance 2 | 3 | 4 | 5 | * [Contributors](#contributors) 6 | * [Maintainers](#maintainers) 7 | * [Governance Committee](#governance-committee) 8 | 9 | 10 | 11 | ## Contributors 12 | 13 | Anyone may be a contributor to TBD projects. Contribution may take the form of: 14 | 15 | * Asking and answering questions on the Discord or GitHub Issues 16 | * Filing an issue 17 | * Offering a feature or bug fix via a Pull Request 18 | * Suggesting documentation improvements 19 | * ...and more! 20 | 21 | Anyone with a GitHub account may use the project issue trackers and communications channels. We welcome newcomers, so don't hesitate to say hi! 22 | 23 | ## Maintainers 24 | 25 | Maintainers have write access to GitHub repositories and act as project administrators. They approve and merge pull requests, cut releases, and guide collaboration with the community. They have: 26 | 27 | * Commit access to their project's repositories 28 | * Write access to continuous integration (CI) jobs 29 | 30 | Both maintainers and non-maintainers may propose changes to 31 | source code. The mechanism to propose such a change is a GitHub pull request. Maintainers review and merge (_land_) pull requests. 32 | 33 | If a maintainer opposes a proposed change, then the change cannot land. The exception is if the Governance Committee (GC) votes to approve the change despite the opposition. Usually, involving the GC is unnecessary. 34 | 35 | See: 36 | 37 | * [List of maintainers - `MAINTAINERS.md`](./MAINTAINERS.md) 38 | * [Contribution Guide - `CONTRIBUTING.md`](./CONTRIBUTING.md) 39 | 40 | ### Maintainer activities 41 | 42 | * Helping users and novice contributors 43 | * Contributing code and documentation changes that improve the project 44 | * Reviewing and commenting on issues and pull requests 45 | * Participation in working groups 46 | * Merging pull requests 47 | 48 | ## Governance Committee 49 | 50 | The TBD Open Source Governance Committee (GC) has final authority over this project, including: 51 | 52 | * Technical direction 53 | * Project governance and process (including this policy) 54 | * Contribution policy 55 | * GitHub repository hosting 56 | * Conduct guidelines 57 | * Maintaining the list of maintainers 58 | 59 | The current GC members are: 60 | 61 | * Ben Boeser, Technical Partnerships Lead, TBD 62 | * Angie Jones, Head of Developer Relations, TBD 63 | * Julie Kim, Head of Legal, TBD 64 | * Nidhi Nahar, Head of Patents and Open Source, Block 65 | * Andrew Lee Rubinger, Head of Open Source, TBD 66 | 67 | Members are not to be contacted individually. The GC may be reached through `tbd-open-source-governance@squareup.com` and is an available resource in mediation or for sensitive cases beyond the scope of project maintainers. It operates as a "Self-appointing council or board" as defined by Red Hat: [Open Source Governance Models](https://www.redhat.com/en/blog/understanding-open-source-governance-models). 68 | -------------------------------------------------------------------------------- /Justfile: -------------------------------------------------------------------------------- 1 | lint: 2 | @echo "Running linter..." 3 | @golangci-lint run 4 | 5 | test: 6 | @echo "Running tests..." 7 | @go clean -testcache && go test -cover ./... 8 | 9 | build: 10 | @echo "Building..." 11 | @go build ./... 12 | 13 | submodule: 14 | @git submodule update --remote --merge -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # web5-go 2 | 3 | # Table of Contents 4 | - [Summary](#summary) 5 | - [`crypto`](#crypto) 6 | - [`dids`](#dids) 7 | - [`jws`](#jws) 8 | - [`jwt`](#jwt) 9 | - [Development](#development) 10 | - [Prerequisites](#prerequisites) 11 | - [`hermit`](#hermit) 12 | - [Helpful Commands](#helpful-commands) 13 | - [`web5` CLI](#web5-cli) 14 | - [Contributing](#contributing) 15 | 16 | 17 | # Summary 18 | This repo contains the following packages: 19 | | package | description | 20 | | :-------------------- | :------------------------------------------------------------------------------------------------------- | 21 | | [`crypto`](./crypto/) | Key Generation, signing, verification, and a Key Manager abstraction | 22 | | [`dids`](./dids/) | DID creation and resolution. | 23 | | [`jwk`](./jwk/) | implements a subset of the [JSON Web Key spec](https://tools.ietf.org/html/rfc7517) | 24 | | [`jws`](./jws/) | [JWS](https://datatracker.ietf.org/doc/html/rfc7515) (JSON Web Signature) signing and verification | 25 | | [`jwt`](./jwt/) | [JWT](https://datatracker.ietf.org/doc/html/rfc7519) (JSON Web Token) parsing, signing, and verification | 26 | 27 | 28 | > [!IMPORTANT] 29 | > Check the README in each directory for more details 30 | 31 | 32 | ## `crypto` 33 | Supported Digital Signature Algorithms: 34 | * [`secp256k1`](https://en.bitcoin.it/wiki/Secp256k1) 35 | * [`Ed25519`](https://datatracker.ietf.org/doc/html/rfc8032#section-5.1) 36 | 37 | ## `dids` 38 | Supported DID Methods: 39 | * [`did:jwk`](https://github.com/quartzjer/did-jwk/blob/main/spec.md) 40 | * 🚧 [`did:dht`](https://github.com/decentralized-identity/did-dht-method) 🚧 41 | 42 | ## `jws` 43 | JWS signing and verification using DIDs 44 | 45 | ## `jwt` 46 | JWT signing and verification using DIDs 47 | 48 | # Development 49 | 50 | ## Prerequisites 51 | We use a submodule for test vectors that make sure we follow the appropriate spec. Running tests will fail without it. 52 | To set up the submodule, clone using: 53 | 54 | ``` 55 | git clone --recurse-submodules git@github.com:decentralized-identity/web5-go.git 56 | ``` 57 | 58 | If you've already cloned, add submodules: 59 | 60 | ``` 61 | git submodule update --init 62 | ``` 63 | 64 | ### [`hermit`](https://cashapp.github.io/hermit/) 65 | This repo uses hermit to manage all environment dependencies (e.g. `just`, `go`). 66 | 67 | > [!IMPORTANT] 68 | > run `. ./bin/activate-hermit` _everytime_ you enter this directory if you don't have hermit [shell hooks](https://cashapp.github.io/hermit/usage/shell/#shell-hooks) configured 69 | 70 | ### Git hooks 71 | Before contributing, set up the pre-commit hook by running: 72 | 73 | ```bash 74 | cp .githooks/pre-commit .git/hooks/pre-commit 75 | chmod +x .git/hooks/pre-commit 76 | ``` 77 | 78 | ### Helpful Commands 79 | 80 | This repo uses [`just`](https://github.com/casey/just) as a command runner. Below is a table of helpful `just` commands: 81 | 82 | | command | description | 83 | | ----------- | -------------- | 84 | | `just test` | runs all tests | 85 | | `just lint` | runs linter | 86 | 87 | ### `web5` CLI 88 | 89 | ```shell 90 | web5 -h 91 | ``` 92 | 93 | See [cmd/web5/README.md](cmd/web5/README.md) for more information. 94 | 95 | ### Contributing 96 | Each package's README contains in-depth information about the package's structure and suggestions on how add features specific to that package -------------------------------------------------------------------------------- /WEB5.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "crypto" 5 | }, 6 | { 7 | "path": "dids" 8 | }, 9 | { 10 | "path": "jwk" 11 | }, 12 | { 13 | "path": "jws" 14 | }, 15 | { 16 | "path": "jwt" 17 | }, 18 | { 19 | "path": "vc" 20 | }, 21 | { 22 | "path": "cmd/web5", 23 | "name": "cli" 24 | }, 25 | { 26 | "path": "." 27 | } 28 | ], 29 | "settings": { 30 | "files.exclude": { 31 | "**/.git": true, 32 | "**/.svn": true, 33 | "**/.hg": true, 34 | "**/CVS": true, 35 | "**/.DS_Store": true, 36 | "**/Thumbs.db": true, 37 | "examples/**/_ftl": true, 38 | "**/node_modules": true, 39 | "**/go.work": true, 40 | "**/go.work.sum": true, 41 | ".hermit": true, 42 | "**/*.zip": true, 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /bin/.go-1.22.0.pkg: -------------------------------------------------------------------------------- 1 | hermit -------------------------------------------------------------------------------- /bin/.golangci-lint-1.56.2.pkg: -------------------------------------------------------------------------------- 1 | hermit -------------------------------------------------------------------------------- /bin/.just-1.23.0.pkg: -------------------------------------------------------------------------------- 1 | hermit -------------------------------------------------------------------------------- /bin/README.hermit.md: -------------------------------------------------------------------------------- 1 | # Hermit environment 2 | 3 | This is a [Hermit](https://github.com/cashapp/hermit) bin directory. 4 | 5 | The symlinks in this directory are managed by Hermit and will automatically 6 | download and install Hermit itself as well as packages. These packages are 7 | local to this environment. 8 | -------------------------------------------------------------------------------- /bin/activate-hermit: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # This file must be used with "source bin/activate-hermit" from bash or zsh. 3 | # You cannot run it directly 4 | # 5 | # THIS FILE IS GENERATED; DO NOT MODIFY 6 | 7 | if [ "${BASH_SOURCE-}" = "$0" ]; then 8 | echo "You must source this script: \$ source $0" >&2 9 | exit 33 10 | fi 11 | 12 | BIN_DIR="$(dirname "${BASH_SOURCE[0]:-${(%):-%x}}")" 13 | if "${BIN_DIR}/hermit" noop > /dev/null; then 14 | eval "$("${BIN_DIR}/hermit" activate "${BIN_DIR}/..")" 15 | 16 | if [ -n "${BASH-}" ] || [ -n "${ZSH_VERSION-}" ]; then 17 | hash -r 2>/dev/null 18 | fi 19 | 20 | echo "Hermit environment $("${HERMIT_ENV}"/bin/hermit env HERMIT_ENV) activated" 21 | fi 22 | -------------------------------------------------------------------------------- /bin/go: -------------------------------------------------------------------------------- 1 | .go-1.22.0.pkg -------------------------------------------------------------------------------- /bin/gofmt: -------------------------------------------------------------------------------- 1 | .go-1.22.0.pkg -------------------------------------------------------------------------------- /bin/golangci-lint: -------------------------------------------------------------------------------- 1 | .golangci-lint-1.56.2.pkg -------------------------------------------------------------------------------- /bin/hermit: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # THIS FILE IS GENERATED; DO NOT MODIFY 4 | 5 | set -eo pipefail 6 | 7 | export HERMIT_USER_HOME=~ 8 | 9 | if [ -z "${HERMIT_STATE_DIR}" ]; then 10 | case "$(uname -s)" in 11 | Darwin) 12 | export HERMIT_STATE_DIR="${HERMIT_USER_HOME}/Library/Caches/hermit" 13 | ;; 14 | Linux) 15 | export HERMIT_STATE_DIR="${XDG_CACHE_HOME:-${HERMIT_USER_HOME}/.cache}/hermit" 16 | ;; 17 | esac 18 | fi 19 | 20 | export HERMIT_DIST_URL="${HERMIT_DIST_URL:-https://github.com/cashapp/hermit/releases/download/stable}" 21 | HERMIT_CHANNEL="$(basename "${HERMIT_DIST_URL}")" 22 | export HERMIT_CHANNEL 23 | export HERMIT_EXE=${HERMIT_EXE:-${HERMIT_STATE_DIR}/pkg/hermit@${HERMIT_CHANNEL}/hermit} 24 | 25 | if [ ! -x "${HERMIT_EXE}" ]; then 26 | echo "Bootstrapping ${HERMIT_EXE} from ${HERMIT_DIST_URL}" 1>&2 27 | INSTALL_SCRIPT="$(mktemp)" 28 | # This value must match that of the install script 29 | INSTALL_SCRIPT_SHA256="180e997dd837f839a3072a5e2f558619b6d12555cd5452d3ab19d87720704e38" 30 | if [ "${INSTALL_SCRIPT_SHA256}" = "BYPASS" ]; then 31 | curl -fsSL "${HERMIT_DIST_URL}/install.sh" -o "${INSTALL_SCRIPT}" 32 | else 33 | # Install script is versioned by its sha256sum value 34 | curl -fsSL "${HERMIT_DIST_URL}/install-${INSTALL_SCRIPT_SHA256}.sh" -o "${INSTALL_SCRIPT}" 35 | # Verify install script's sha256sum 36 | openssl dgst -sha256 "${INSTALL_SCRIPT}" | \ 37 | awk -v EXPECTED="$INSTALL_SCRIPT_SHA256" \ 38 | '$2!=EXPECTED {print "Install script sha256 " $2 " does not match " EXPECTED; exit 1}' 39 | fi 40 | /bin/bash "${INSTALL_SCRIPT}" 1>&2 41 | fi 42 | 43 | exec "${HERMIT_EXE}" --level=fatal exec "$0" -- "$@" 44 | -------------------------------------------------------------------------------- /bin/hermit.hcl: -------------------------------------------------------------------------------- 1 | env = { 2 | "PATH": "${HERMIT_ENV}/scripts:${PATH}", 3 | } -------------------------------------------------------------------------------- /bin/just: -------------------------------------------------------------------------------- 1 | .just-1.23.0.pkg -------------------------------------------------------------------------------- /cmd/web5/README.md: -------------------------------------------------------------------------------- 1 | # web5 2 | 3 | ## Usage 4 | 5 | ```shell 6 | ➜ web5 -h 7 | Usage: web5 8 | 9 | Web5 - A decentralized web platform that puts you in control of your data and identity. 10 | 11 | Flags: 12 | -h, --help Show context-sensitive help. 13 | 14 | Commands: 15 | jwt sign 16 | Sign a JWT. 17 | 18 | jwt decode 19 | Decode a JWT. 20 | 21 | jwt verify 22 | Verify a JWT. 23 | 24 | did resolve 25 | Resolve a DID. 26 | 27 | did create jwk 28 | Create a did:jwk. 29 | 30 | did create web 31 | Create a did:web. 32 | 33 | vc create 34 | Create a VC. 35 | 36 | vc sign 37 | Sign a VC. 38 | 39 | vcjwt verify 40 | Verify a VC-JWT. 41 | 42 | vcjwt decode 43 | Decode a VC-JWT. 44 | 45 | did create dht 46 | Create did:dht's using the default gateway. 47 | 48 | Run "web5 --help" for more information on a command. 49 | ``` 50 | -------------------------------------------------------------------------------- /cmd/web5/cmd_did_create.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | 8 | "github.com/decentralized-identity/web5-go/dids/did" 9 | "github.com/decentralized-identity/web5-go/dids/diddht" 10 | "github.com/decentralized-identity/web5-go/dids/didjwk" 11 | "github.com/decentralized-identity/web5-go/dids/didweb" 12 | ) 13 | 14 | type didCreateCMD struct { 15 | JWK didCreateJWKCMD `cmd:"" help:"Create a did:jwk."` 16 | Web didCreateWebCMD `cmd:"" help:"Create a did:web."` 17 | DHT didCreateDHTCMD `cmd:"" help:"Create a did:dht."` 18 | } 19 | 20 | type didCreateJWKCMD struct { 21 | NoIndent bool `help:"Print the portable DID without indentation." default:"false"` 22 | } 23 | 24 | func (c *didCreateJWKCMD) Run() error { 25 | did, err := didjwk.Create() 26 | if err != nil { 27 | return err 28 | } 29 | 30 | return printDID(did, c.NoIndent) 31 | } 32 | 33 | type didCreateWebCMD struct { 34 | Domain string `arg:"" help:"The domain name for the DID." required:""` 35 | NoIndent bool `help:"Print the portable DID without indentation." default:"false"` 36 | } 37 | 38 | func (c *didCreateWebCMD) Run() error { 39 | did, err := didweb.Create(c.Domain) 40 | if err != nil { 41 | return err 42 | } 43 | 44 | return printDID(did, c.NoIndent) 45 | } 46 | 47 | type didCreateDHTCMD struct { 48 | NoIndent bool `help:"Print the portable DID without indentation." default:"false"` 49 | } 50 | 51 | func (c *didCreateDHTCMD) Run() error { 52 | did, err := diddht.CreateWithContext(context.Background()) 53 | if err != nil { 54 | return err 55 | } 56 | 57 | return printDID(did, c.NoIndent) 58 | } 59 | 60 | func printDID(d did.BearerDID, noIndent bool) error { 61 | portableDID, err := d.ToPortableDID() 62 | if err != nil { 63 | return err 64 | } 65 | 66 | var jsonDID []byte 67 | if noIndent { 68 | jsonDID, err = json.Marshal(portableDID) 69 | } else { 70 | jsonDID, err = json.MarshalIndent(portableDID, "", " ") 71 | } 72 | 73 | if err != nil { 74 | return err 75 | } 76 | 77 | fmt.Println(string(jsonDID)) 78 | 79 | return nil 80 | } 81 | -------------------------------------------------------------------------------- /cmd/web5/cmd_did_resolve.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/decentralized-identity/web5-go/dids" 8 | ) 9 | 10 | type didResolveCMD struct { 11 | URI string `arg:"" name:"uri" help:"The URI to resolve."` 12 | NoIndent bool `help:"Print the DID Document without indentation." default:"false"` 13 | } 14 | 15 | func (c *didResolveCMD) Run() error { 16 | result, err := dids.Resolve(c.URI) 17 | if err != nil { 18 | return err 19 | } 20 | 21 | var jsonDIDDocument []byte 22 | if c.NoIndent { 23 | jsonDIDDocument, err = json.Marshal(result.Document) 24 | } else { 25 | jsonDIDDocument, err = json.MarshalIndent(result.Document, "", " ") 26 | } 27 | if err != nil { 28 | return err 29 | } 30 | 31 | fmt.Println(string(jsonDIDDocument)) 32 | 33 | return nil 34 | } 35 | -------------------------------------------------------------------------------- /cmd/web5/cmd_jwt_decode.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/decentralized-identity/web5-go/jwt" 8 | ) 9 | 10 | type jwtDecodeCMD struct { 11 | JWT string `arg:"" help:"The base64 encoded JWT"` 12 | Claims bool `help:"Only print the JWT Claims." default:"false"` 13 | NoIndent bool `help:"Print the decoded JWT without indentation." default:"false"` 14 | } 15 | 16 | func (c *jwtDecodeCMD) Run() error { 17 | decoded, err := jwt.Decode(c.JWT) 18 | if err != nil { 19 | return err 20 | } 21 | 22 | var partToPrint any 23 | if c.Claims { 24 | partToPrint = decoded.Claims 25 | } else { 26 | partToPrint = decoded 27 | } 28 | 29 | var bytes []byte 30 | if c.NoIndent { 31 | bytes, err = json.Marshal(partToPrint) 32 | } else { 33 | bytes, err = json.MarshalIndent(partToPrint, "", " ") 34 | } 35 | if err != nil { 36 | return err 37 | } 38 | 39 | fmt.Println(string(bytes)) 40 | 41 | return nil 42 | } 43 | -------------------------------------------------------------------------------- /cmd/web5/cmd_jwt_sign.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/decentralized-identity/web5-go/dids/did" 8 | "github.com/decentralized-identity/web5-go/jwt" 9 | ) 10 | 11 | type jwtSignCMD struct { 12 | Claims string `arg:"" help:"The JWT Claims. Value is a JSON string."` 13 | PortableDID string `arg:"" help:"The Portable DID to sign with. Value is a JSON string."` 14 | Purpose string `help:"Used to specify which key from the given DID Document should be used."` 15 | Type string `help:"Used to set the JWS Header 'typ' property"` 16 | } 17 | 18 | func (c *jwtSignCMD) Run() error { 19 | var claims jwt.Claims 20 | err := json.Unmarshal([]byte(c.Claims), &claims) 21 | if err != nil { 22 | return fmt.Errorf("%s: %w", "invalid credential", err) 23 | } 24 | 25 | var portableDID did.PortableDID 26 | err = json.Unmarshal([]byte(c.PortableDID), &portableDID) 27 | if err != nil { 28 | return fmt.Errorf("%s: %w", "invalid portable DID", err) 29 | } 30 | 31 | bearerDID, err := did.FromPortableDID(portableDID) 32 | if err != nil { 33 | return err 34 | } 35 | 36 | opts := []jwt.SignOpt{} 37 | if c.Purpose != "" { 38 | opts = append(opts, jwt.Purpose(c.Purpose)) 39 | } 40 | if c.Type != "" { 41 | opts = append(opts, jwt.Type(c.Type)) 42 | } 43 | 44 | signed, err := jwt.Sign(claims, bearerDID, opts...) 45 | if err != nil { 46 | return err 47 | } 48 | 49 | fmt.Println(signed) 50 | 51 | return nil 52 | } 53 | -------------------------------------------------------------------------------- /cmd/web5/cmd_jwt_verify.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/decentralized-identity/web5-go/jwt" 8 | ) 9 | 10 | type jwtVerifyCMD struct { 11 | JWT string `arg:"" help:"The base64 encoded JWT"` 12 | Claims bool `help:"Only print the JWT Claims." default:"false"` 13 | NoIndent bool `help:"Print the decoded JWT without indentation." default:"false"` 14 | } 15 | 16 | func (c *jwtVerifyCMD) Run() error { 17 | decoded, err := jwt.Verify(c.JWT) 18 | if err != nil { 19 | return err 20 | } 21 | 22 | var partToPrint any 23 | if c.Claims { 24 | partToPrint = decoded.Claims 25 | } else { 26 | partToPrint = decoded 27 | } 28 | 29 | var bytes []byte 30 | if c.NoIndent { 31 | bytes, err = json.Marshal(partToPrint) 32 | } else { 33 | bytes, err = json.MarshalIndent(partToPrint, "", " ") 34 | } 35 | if err != nil { 36 | return err 37 | } 38 | 39 | fmt.Println(string(bytes)) 40 | 41 | return nil 42 | } 43 | -------------------------------------------------------------------------------- /cmd/web5/cmd_vc_create.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/decentralized-identity/web5-go/dids/did" 9 | "github.com/decentralized-identity/web5-go/vc" 10 | ) 11 | 12 | type vcCreateCMD struct { 13 | CredentialSubjectID string `arg:"" help:"The Credential Subject's ID"` 14 | Claims string `help:"Add additional credentialSubject claims (JSON string). ex: '{\"name\": \"John Doe\"}'." default:"{}"` 15 | Contexts []string `help:"Add additional @context's to the default [\"https://www.w3.org/2018/credentials/v1\"]."` 16 | Types []string `help:"Add additional type's to the default [\"VerifiableCredential\"]."` 17 | ID string `help:"Override the default ID of format urn:vc:uuid:."` 18 | IssuanceDate time.Time `help:"Override the default issuanceDate of time.Now()."` 19 | ExpirationDate time.Time `help:"Override the default expirationDate of nil."` 20 | Sign string `help:"Portable DID used to sign the VC-JWT. Value is a JSON string."` 21 | NoIndent bool `help:"Print the VC without indentation." default:"false"` 22 | } 23 | 24 | func (c *vcCreateCMD) Run() error { 25 | opts := []vc.CreateOption{} 26 | if len(c.Contexts) > 0 { 27 | opts = append(opts, vc.Contexts(c.Contexts...)) 28 | } 29 | if len(c.Types) > 0 { 30 | opts = append(opts, vc.Types(c.Types...)) 31 | } 32 | if c.ID != "" { 33 | opts = append(opts, vc.ID(c.ID)) 34 | } 35 | if (c.IssuanceDate != time.Time{}) { 36 | opts = append(opts, vc.IssuanceDate(c.IssuanceDate)) 37 | } 38 | if (c.ExpirationDate != time.Time{}) { 39 | opts = append(opts, vc.ExpirationDate(c.ExpirationDate)) 40 | } 41 | 42 | var claims vc.Claims 43 | err := json.Unmarshal([]byte(c.Claims), &claims) 44 | if err != nil { 45 | return fmt.Errorf("%s: %w", "invalid claims", err) 46 | } 47 | 48 | claims["id"] = c.CredentialSubjectID 49 | 50 | credential := vc.Create(claims, opts...) 51 | 52 | if c.Sign != "" { 53 | var portableDID did.PortableDID 54 | err = json.Unmarshal([]byte(c.Sign), &portableDID) 55 | if err != nil { 56 | return fmt.Errorf("%s: %w", "invalid portable DID", err) 57 | } 58 | 59 | bearerDID, err := did.FromPortableDID(portableDID) 60 | if err != nil { 61 | return err 62 | } 63 | 64 | // TODO sign opts 65 | signed, err := credential.Sign(bearerDID) 66 | if err != nil { 67 | return err 68 | } 69 | 70 | fmt.Println(signed) 71 | 72 | return nil 73 | } 74 | 75 | var jsonVC []byte 76 | if c.NoIndent { 77 | jsonVC, err = json.Marshal(credential) 78 | } else { 79 | jsonVC, err = json.MarshalIndent(credential, "", " ") 80 | } 81 | if err != nil { 82 | return err 83 | } 84 | 85 | fmt.Println(string(jsonVC)) 86 | 87 | return nil 88 | } 89 | -------------------------------------------------------------------------------- /cmd/web5/cmd_vc_sign.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/decentralized-identity/web5-go/dids/did" 8 | "github.com/decentralized-identity/web5-go/vc" 9 | ) 10 | 11 | type vcSignCMD struct { 12 | VC string `arg:"" help:"The VC to sign. Value is a JSON string."` 13 | PortableDID string `arg:"" help:"The Portable DID to sign with. Value is a JSON string."` 14 | } 15 | 16 | func (c *vcSignCMD) Run() error { 17 | var credential vc.DataModel[vc.Claims] 18 | err := json.Unmarshal([]byte(c.VC), &credential) 19 | if err != nil { 20 | return fmt.Errorf("%s: %w", "invalid credential", err) 21 | } 22 | 23 | var portableDID did.PortableDID 24 | err = json.Unmarshal([]byte(c.PortableDID), &portableDID) 25 | if err != nil { 26 | return fmt.Errorf("%s: %w", "invalid portable DID", err) 27 | } 28 | 29 | bearerDID, err := did.FromPortableDID(portableDID) 30 | if err != nil { 31 | return err 32 | } 33 | 34 | // TODO sign opts 35 | signed, err := credential.Sign(bearerDID) 36 | if err != nil { 37 | return err 38 | } 39 | 40 | fmt.Println(signed) 41 | 42 | return nil 43 | } 44 | -------------------------------------------------------------------------------- /cmd/web5/cmd_vcjwt_decode.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/decentralized-identity/web5-go/vc" 8 | ) 9 | 10 | type vcjwtDecodeCMD struct { 11 | JWT string `arg:"" help:"The VC-JWT"` 12 | NoIndent bool `help:"Print the decoded VC-JWT without indentation." default:"false"` 13 | } 14 | 15 | func (c *vcjwtDecodeCMD) Run() error { 16 | decoded, err := vc.Decode[vc.Claims](c.JWT) 17 | if err != nil { 18 | return err 19 | } 20 | 21 | var jsonVC []byte 22 | if c.NoIndent { 23 | jsonVC, err = json.Marshal(decoded.VC) 24 | } else { 25 | jsonVC, err = json.MarshalIndent(decoded.VC, "", " ") 26 | } 27 | if err != nil { 28 | return err 29 | } 30 | 31 | fmt.Println(string(jsonVC)) 32 | 33 | return nil 34 | } 35 | -------------------------------------------------------------------------------- /cmd/web5/cmd_vcjwt_verify.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/decentralized-identity/web5-go/vc" 8 | ) 9 | 10 | type vcjwtVerifyCMD struct { 11 | JWT string `arg:"" help:"The VC-JWT"` 12 | NoIndent bool `help:"Print the decoded VC-JWT without indentation." default:"false"` 13 | } 14 | 15 | func (c *vcjwtVerifyCMD) Run() error { 16 | decoded, err := vc.Verify[vc.Claims](c.JWT) 17 | if err != nil { 18 | return err 19 | } 20 | 21 | var jsonVC []byte 22 | if c.NoIndent { 23 | jsonVC, err = json.Marshal(decoded.VC) 24 | } else { 25 | jsonVC, err = json.MarshalIndent(decoded.VC, "", " ") 26 | } 27 | if err != nil { 28 | return err 29 | } 30 | 31 | fmt.Println(string(jsonVC)) 32 | 33 | return nil 34 | } 35 | -------------------------------------------------------------------------------- /cmd/web5/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/alecthomas/kong" 7 | ) 8 | 9 | // CLI is the main command line interface for the web5 CLI. 10 | // more information about this struct can be found in the [kong documentation] 11 | // 12 | // [kong documentation]: https://github.com/alecthomas/kong 13 | type CLI struct { 14 | JWT struct { 15 | Sign jwtSignCMD `cmd:"" help:"Sign a JWT."` 16 | Decode jwtDecodeCMD `cmd:"" help:"Decode a JWT."` 17 | Verify jwtVerifyCMD `cmd:"" help:"Verify a JWT."` 18 | } `cmd:"" help:"Interface with JWT's."` 19 | DID struct { 20 | Resolve didResolveCMD `cmd:"" help:"Resolve a DID."` 21 | Create didCreateCMD `cmd:"" help:"Create a DID."` 22 | } `cmd:"" help:"Interface with DID's."` 23 | VC struct { 24 | Create vcCreateCMD `cmd:"" help:"Create a VC."` 25 | Sign vcSignCMD `cmd:"" help:"Sign a VC."` 26 | } `cmd:"" help:"Interface with VC's."` 27 | VCJWT struct { 28 | Verify vcjwtVerifyCMD `cmd:"" help:"Verify a VC-JWT."` 29 | Decode vcjwtDecodeCMD `cmd:"" help:"Decode a VC-JWT."` 30 | } `cmd:"" help:"Interface with VC-JWT's."` 31 | } 32 | 33 | func main() { 34 | kctx := kong.Parse(&CLI{}, 35 | kong.Description("Web5 - A decentralized web platform that puts you in control of your data and identity."), 36 | ) 37 | 38 | ctx := context.Background() 39 | kctx.BindTo(ctx, (*context.Context)(nil)) 40 | err := kctx.Run(ctx) 41 | kctx.FatalIfErrorf(err) 42 | } 43 | -------------------------------------------------------------------------------- /crypto/README.md: -------------------------------------------------------------------------------- 1 | # `crypto` 2 | 3 | This package mostly exists to maintain parity with the structure of other web5 SDKs maintainted by TBD. Check out the [dsa](./dsa) package for supported Digital Signature Algorithms 4 | 5 | > [!NOTE] 6 | > If the need arises, this package will also contain cryptographic primitives for encryption 7 | 8 | # Table of Contents 9 | 10 | - [Features](#features) 11 | - [Usage](#usage) 12 | - [`dsa`](#dsa) 13 | - [Key Generation](#key-generation) 14 | - [Signing](#signing) 15 | - [Verifying](#verifying) 16 | - [Directory Structure](#directory-structure) 17 | - [Rationale](#rationale) 18 | 19 | 20 | # Features 21 | * secp256k1 keygen, deterministic signing, and verification 22 | * ed25519 keygen, signing, and verification 23 | * higher-level API for `ecdsa` (Elliptic Curve Digital Signature Algorithm) 24 | * higher-level API for `eddsa` (Edwards-Curve Digital Signature Algorithm) 25 | * higher level API for `dsa` in general (Digital Signature Algorithm) 26 | * `KeyManager` interface that can leveraged to manage/use keys (create, sign etc) as desired per the given use case. examples of concrete implementations include: AWS KMS, Azure Key Vault, Google Cloud KMS, Hashicorp Vault etc 27 | * Concrete implementation of `KeyManager` that stores keys in memory 28 | 29 | 30 | 31 | # Usage 32 | 33 | ## `dsa` 34 | 35 | ### Key Generation 36 | 37 | the `dsa` package provides [algorithm IDs](https://github.com/decentralized-identity/web5-go/blob/5d50ce8f24e4b47b0a8626724e8a571e9b5c847f/crypto/dsa/dsa.go#L11-L14) that can be passed to the `GenerateKey` function e.g. 38 | 39 | ```go 40 | package main 41 | 42 | import ( 43 | "fmt" 44 | 45 | "github.com/decentralized-identity/web5-go/crypto/dsa" 46 | ) 47 | 48 | func main() { 49 | privateJwk, err := dsa.GeneratePrivateKey(dsa.AlgorithmIDSECP256K1) 50 | if err != nil { 51 | fmt.Printf("Failed to generate private key: %v\n", err) 52 | return 53 | } 54 | } 55 | ``` 56 | 57 | ### Signing 58 | 59 | Signing takes a private key and a payload to sign. e.g. 60 | 61 | ```go 62 | package main 63 | 64 | import ( 65 | "fmt" 66 | 67 | "github.com/decentralized-identity/web5-go/crypto/dsa" 68 | ) 69 | 70 | func main() { 71 | // Generate private key 72 | privateJwk, err := dsa.GeneratePrivateKey(dsa.AlgorithmIDSECP256K1) 73 | if err != nil { 74 | fmt.Printf("Failed to generate private key: %v\n", err) 75 | return 76 | } 77 | 78 | // Payload to be signed 79 | payload := []byte("hello world") 80 | 81 | // Signing the payload 82 | signature, err := dsa.Sign(payload, privateJwk) 83 | if err != nil { 84 | fmt.Printf("Failed to sign: %v\n", err) 85 | return 86 | } 87 | } 88 | ``` 89 | 90 | ### Verifying 91 | Verifying takes a public key, the payload that was signed, and the signature. e.g. 92 | 93 | ```go 94 | package main 95 | 96 | import ( 97 | "fmt" 98 | "github.com/decentralized-identity/web5-go/crypto/dsa" 99 | ) 100 | 101 | func main() { 102 | // Generate ED25519 private key 103 | privateJwk, err := dsa.GeneratePrivateKey(dsa.AlgorithmIDED25519) 104 | if err != nil { 105 | fmt.Printf("Failed to generate private key: %v\n", err) 106 | return 107 | } 108 | 109 | // Payload to be signed 110 | payload := []byte("hello world") 111 | 112 | // Sign the payload 113 | signature, err := dsa.Sign(payload, privateJwk) 114 | if err != nil { 115 | fmt.Printf("Failed to sign: %v\n", err) 116 | return 117 | } 118 | 119 | // Get the public key from the private key 120 | publicJwk := dsa.GetPublicKey(privateJwk) 121 | 122 | // Verify the signature 123 | legit, err := dsa.Verify(payload, signature, publicJwk) 124 | if err != nil { 125 | fmt.Printf("Failed to verify: %v\n", err) 126 | return 127 | } 128 | 129 | if !legit { 130 | fmt.Println("Failed to verify signature") 131 | } else { 132 | fmt.Println("Signature verified successfully") 133 | } 134 | } 135 | ``` 136 | 137 | > [!NOTE] 138 | > `ecdsa` and `eddsa` provide the same high level api as `dsa`, but specifically for algorithms within those respective families. this makes it so that if you add an additional algorithm, it automatically gets picked up by `dsa` as well. 139 | 140 | 141 | # Directory Structure 142 | 143 | ``` 144 | crypto 145 | ├── README.md 146 | ├── doc.go 147 | ├── dsa 148 | │   ├── README.md 149 | │   ├── dsa.go 150 | │   ├── dsa_test.go 151 | │   ├── ecdsa 152 | │   │   ├── ecdsa.go 153 | │   │   ├── secp256k1.go 154 | │   │   └── secp256k1_test.go 155 | │   └── eddsa 156 | │   ├── ed25519.go 157 | │   └── eddsa.go 158 | ├── keymanager.go 159 | └── keymanager_test.go 160 | ``` 161 | 162 | ## Rationale 163 | _Why compartmentalize `dsa`?_ 164 | 165 | to make room for non signature related crypto in the future if need be 166 | 167 | --- 168 | 169 | _why compartmentalize `ecdsa` and `eddsa` ?_ 170 | 171 | * because it's a family of algorithms have common behavior (e.g. private key -> public key) 172 | * to make it easier to add future algorithm support down the line e.g. `secp256r1`, `ed448` -------------------------------------------------------------------------------- /crypto/doc.go: -------------------------------------------------------------------------------- 1 | // Package crypto provides the following functionality: 2 | // * Key Generation: secp256k1, ed25519 3 | // * Signing: secp256k1, ed25519 4 | // * Verification: secp256k1, ed25519 5 | // * A KeyManager abstraction that can be leveraged to manage/use keys (create, sign etc) as desired per the given use case 6 | package crypto 7 | -------------------------------------------------------------------------------- /crypto/dsa/dsa.go: -------------------------------------------------------------------------------- 1 | package dsa 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/decentralized-identity/web5-go/crypto/dsa/ecdsa" 7 | "github.com/decentralized-identity/web5-go/crypto/dsa/eddsa" 8 | "github.com/decentralized-identity/web5-go/jwk" 9 | ) 10 | 11 | const ( 12 | AlgorithmIDSECP256K1 = ecdsa.SECP256K1AlgorithmID 13 | AlgorithmIDED25519 = eddsa.ED25519AlgorithmID 14 | ) 15 | 16 | // GeneratePrivateKey generates a private key using the algorithm specified by algorithmID. 17 | func GeneratePrivateKey(algorithmID string) (jwk.JWK, error) { 18 | if ecdsa.SupportsAlgorithmID(algorithmID) { 19 | return ecdsa.GeneratePrivateKey(algorithmID) 20 | } else if eddsa.SupportsAlgorithmID(algorithmID) { 21 | return eddsa.GeneratePrivateKey(algorithmID) 22 | } 23 | 24 | return jwk.JWK{}, fmt.Errorf("unsupported algorithm: %s", algorithmID) 25 | } 26 | 27 | // GetPublicKey returns the public key corresponding to the given private key. 28 | func GetPublicKey(privateKey jwk.JWK) jwk.JWK { 29 | switch privateKey.KTY { 30 | case ecdsa.KeyType: 31 | return ecdsa.GetPublicKey(privateKey) 32 | case eddsa.KeyType: 33 | return eddsa.GetPublicKey(privateKey) 34 | default: 35 | return jwk.JWK{} 36 | } 37 | } 38 | 39 | // Sign signs the payload using the given private key. 40 | func Sign(payload []byte, jwk jwk.JWK) ([]byte, error) { 41 | switch jwk.KTY { 42 | case ecdsa.KeyType: 43 | return ecdsa.Sign(payload, jwk) 44 | case eddsa.KeyType: 45 | return eddsa.Sign(payload, jwk) 46 | default: 47 | return nil, fmt.Errorf("unsupported key type: %s", jwk.KTY) 48 | } 49 | } 50 | 51 | // Verify verifies the signature of the payload using the given public key. 52 | func Verify(payload []byte, signature []byte, jwk jwk.JWK) (bool, error) { 53 | switch jwk.KTY { 54 | case ecdsa.KeyType: 55 | return ecdsa.Verify(payload, signature, jwk) 56 | case eddsa.KeyType: 57 | return eddsa.Verify(payload, signature, jwk) 58 | default: 59 | return false, fmt.Errorf("unsupported key type: %s", jwk.KTY) 60 | } 61 | } 62 | 63 | // GetJWA returns the JWA (JSON Web Algorithm) algorithm corresponding to the given key. 64 | func GetJWA(jwk jwk.JWK) (string, error) { 65 | switch jwk.KTY { 66 | case ecdsa.KeyType: 67 | return ecdsa.GetJWA(jwk) 68 | case eddsa.KeyType: 69 | return eddsa.GetJWA(jwk) 70 | default: 71 | return "", fmt.Errorf("unsupported key type: %s", jwk.KTY) 72 | } 73 | } 74 | 75 | // BytesToPublicKey converts the given bytes to a public key based on the algorithm specified by algorithmID. 76 | func BytesToPublicKey(algorithmID string, input []byte) (jwk.JWK, error) { 77 | if ecdsa.SupportsAlgorithmID(algorithmID) { 78 | return ecdsa.BytesToPublicKey(algorithmID, input) 79 | } else if eddsa.SupportsAlgorithmID(algorithmID) { 80 | return eddsa.BytesToPublicKey(algorithmID, input) 81 | } 82 | 83 | return jwk.JWK{}, fmt.Errorf("unsupported algorithm: %s", algorithmID) 84 | } 85 | 86 | // PublicKeyToBytes converts the provided public key to bytes 87 | func PublicKeyToBytes(publicKey jwk.JWK) ([]byte, error) { 88 | switch publicKey.KTY { 89 | case ecdsa.KeyType: 90 | return ecdsa.PublicKeyToBytes(publicKey) 91 | case eddsa.KeyType: 92 | return eddsa.PublicKeyToBytes(publicKey) 93 | default: 94 | return nil, fmt.Errorf("unsupported key type: %s", publicKey.KTY) 95 | } 96 | } 97 | 98 | // AlgorithmID returns the algorithm ID for the given jwk.JWK 99 | func AlgorithmID(jwk *jwk.JWK) (string, error) { 100 | switch jwk.KTY { 101 | case ecdsa.KeyType: 102 | return ecdsa.AlgorithmID(jwk) 103 | case eddsa.KeyType: 104 | return eddsa.AlgorithmID(jwk) 105 | default: 106 | return "", fmt.Errorf("unsupported key type: %s", jwk.KTY) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /crypto/dsa/dsa_test.go: -------------------------------------------------------------------------------- 1 | package dsa_test 2 | 3 | import ( 4 | "encoding/hex" 5 | "testing" 6 | 7 | "github.com/alecthomas/assert/v2" 8 | "github.com/decentralized-identity/web5-go/crypto/dsa" 9 | "github.com/decentralized-identity/web5-go/crypto/dsa/ecdsa" 10 | "github.com/decentralized-identity/web5-go/crypto/dsa/eddsa" 11 | "github.com/decentralized-identity/web5-go/jwk" 12 | ) 13 | 14 | func TestGeneratePrivateKeySECP256K1(t *testing.T) { 15 | privateJwk, err := dsa.GeneratePrivateKey(dsa.AlgorithmIDSECP256K1) 16 | 17 | assert.NoError(t, err) 18 | assert.Equal[string](t, ecdsa.SECP256K1JWACurve, privateJwk.CRV) 19 | assert.Equal[string](t, ecdsa.KeyType, privateJwk.KTY) 20 | assert.True(t, privateJwk.D != "", "privateJwk.D is empty") 21 | assert.True(t, privateJwk.X != "", "privateJwk.X is empty") 22 | assert.True(t, privateJwk.Y != "", "privateJwk.Y is empty") 23 | } 24 | 25 | func TestGeneratePrivateKeyED25519(t *testing.T) { 26 | privateJwk, err := dsa.GeneratePrivateKey(dsa.AlgorithmIDED25519) 27 | if err != nil { 28 | t.Errorf("failed to generate private key: %v", err.Error()) 29 | } 30 | 31 | assert.NoError(t, err) 32 | assert.Equal[string](t, eddsa.ED25519JWACurve, privateJwk.CRV) 33 | assert.Equal[string](t, eddsa.KeyType, privateJwk.KTY) 34 | assert.True(t, privateJwk.D != "", "privateJwk.D is empty") 35 | assert.True(t, privateJwk.X != "", "privateJwk.X is empty") 36 | } 37 | 38 | func TestSignSECP256K1(t *testing.T) { 39 | privateJwk, err := dsa.GeneratePrivateKey(dsa.AlgorithmIDSECP256K1) 40 | assert.NoError(t, err) 41 | 42 | payload := []byte("hello world") 43 | signature, err := dsa.Sign(payload, privateJwk) 44 | 45 | assert.NoError(t, err) 46 | assert.True(t, len(signature) == 64, "invalid signature length") 47 | } 48 | 49 | func TestSignED25519(t *testing.T) { 50 | privateJwk, err := dsa.GeneratePrivateKey(dsa.AlgorithmIDED25519) 51 | assert.NoError(t, err) 52 | 53 | payload := []byte("hello world") 54 | signature, err := dsa.Sign(payload, privateJwk) 55 | 56 | assert.NoError(t, err) 57 | assert.True(t, len(signature) == 64, "invalid signature length") 58 | } 59 | 60 | func TestSignDeterministicSECP256K1(t *testing.T) { 61 | privateJwk, err := dsa.GeneratePrivateKey(dsa.AlgorithmIDSECP256K1) 62 | assert.NoError(t, err) 63 | 64 | payload := []byte("hello world") 65 | signature1, err := dsa.Sign(payload, privateJwk) 66 | assert.NoError(t, err, "failed to sign") 67 | 68 | signature2, err := dsa.Sign(payload, privateJwk) 69 | assert.NoError(t, err) 70 | 71 | assert.Equal(t, signature1, signature2, "signature is not deterministic") 72 | } 73 | 74 | func TestSignDeterministicED25519(t *testing.T) { 75 | privateJwk, err := dsa.GeneratePrivateKey(dsa.AlgorithmIDED25519) 76 | assert.NoError(t, err) 77 | 78 | payload := []byte("hello world") 79 | signature1, err := dsa.Sign(payload, privateJwk) 80 | assert.NoError(t, err, "failed to sign") 81 | 82 | signature2, err := dsa.Sign(payload, privateJwk) 83 | assert.NoError(t, err) 84 | 85 | assert.Equal(t, signature1, signature2, "signature is not deterministic") 86 | } 87 | 88 | func TestVerifySECP256K1(t *testing.T) { 89 | privateJwk, err := dsa.GeneratePrivateKey(dsa.AlgorithmIDSECP256K1) 90 | assert.NoError(t, err) 91 | 92 | payload := []byte("hello world") 93 | signature, err := dsa.Sign(payload, privateJwk) 94 | assert.NoError(t, err) 95 | 96 | publicJwk := dsa.GetPublicKey(privateJwk) 97 | 98 | legit, err := dsa.Verify(payload, signature, publicJwk) 99 | assert.NoError(t, err) 100 | 101 | assert.True(t, legit, "failed to verify signature") 102 | } 103 | 104 | func TestVerifyED25519(t *testing.T) { 105 | privateJwk, err := dsa.GeneratePrivateKey(dsa.AlgorithmIDED25519) 106 | assert.NoError(t, err) 107 | 108 | payload := []byte("hello world") 109 | signature, err := dsa.Sign(payload, privateJwk) 110 | 111 | assert.NoError(t, err) 112 | 113 | publicJwk := dsa.GetPublicKey(privateJwk) 114 | 115 | legit, err := dsa.Verify(payload, signature, publicJwk) 116 | assert.NoError(t, err) 117 | 118 | assert.True(t, legit, "failed to verify signature") 119 | } 120 | 121 | func TestBytesToPublicKey_BadAlgorithm(t *testing.T) { 122 | _, err := dsa.BytesToPublicKey("yolocrypto", []byte{0x00, 0x01, 0x02, 0x03}) 123 | assert.Error(t, err) 124 | } 125 | 126 | func TestBytesToPublicKey_BadBytes(t *testing.T) { 127 | _, err := dsa.BytesToPublicKey(dsa.AlgorithmIDSECP256K1, []byte{0x00, 0x01, 0x02, 0x03}) 128 | assert.Error(t, err) 129 | } 130 | 131 | func TestBytesToPublicKey_SECP256K1(t *testing.T) { 132 | // vector taken from https://github.com/decentralized-identity/web5-js/blob/dids-new-crypto/packages/crypto/tests/fixtures/test-vectors/secp256k1/bytes-to-public-key.json 133 | publicKeyHex := "0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8" 134 | pubKeyBytes, err := hex.DecodeString(publicKeyHex) 135 | assert.NoError(t, err) 136 | 137 | jwk, err := dsa.BytesToPublicKey(dsa.AlgorithmIDSECP256K1, pubKeyBytes) 138 | assert.NoError(t, err) 139 | 140 | assert.Equal(t, ecdsa.SECP256K1JWACurve, jwk.CRV) 141 | assert.Equal(t, ecdsa.KeyType, jwk.KTY) 142 | assert.Equal(t, "eb5mfvncu6xVoGKVzocLBwKb_NstzijZWfKBWxb4F5g", jwk.X) 143 | assert.Equal(t, "SDradyajxGVdpPv8DhEIqP0XtEimhVQZnEfQj_sQ1Lg", jwk.Y) 144 | } 145 | 146 | func TestPublicKeyToBytes_UnsupportedKTY(t *testing.T) { 147 | _, err := dsa.PublicKeyToBytes(jwk.JWK{KTY: "yolocrypto"}) 148 | assert.Error(t, err) 149 | } 150 | -------------------------------------------------------------------------------- /crypto/dsa/ecdsa/ecdsa.go: -------------------------------------------------------------------------------- 1 | package ecdsa 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/decentralized-identity/web5-go/jwk" 8 | ) 9 | 10 | const ( 11 | KeyType = "EC" 12 | ) 13 | 14 | var algorithmIDs = map[string]bool{ 15 | SECP256K1AlgorithmID: true, 16 | } 17 | 18 | // GeneratePrivateKey generates an ECDSA private key for the given algorithm 19 | func GeneratePrivateKey(algorithmID string) (jwk.JWK, error) { 20 | switch algorithmID { 21 | case SECP256K1AlgorithmID: 22 | return SECP256K1GeneratePrivateKey() 23 | default: 24 | return jwk.JWK{}, fmt.Errorf("unsupported algorithm: %s", algorithmID) 25 | } 26 | } 27 | 28 | // GetPublicKey builds an ECDSA public key from the given ECDSA private key 29 | func GetPublicKey(privateKey jwk.JWK) jwk.JWK { 30 | return jwk.JWK{ 31 | KTY: privateKey.KTY, 32 | CRV: privateKey.CRV, 33 | X: privateKey.X, 34 | Y: privateKey.Y, 35 | } 36 | } 37 | 38 | // Sign generates a cryptographic signature for the given payload with the given private key 39 | // 40 | // # Note 41 | // 42 | // The function will automatically detect the given ECDSA cryptographic curve from the given private key 43 | func Sign(payload []byte, privateKey jwk.JWK) ([]byte, error) { 44 | if privateKey.D == "" { 45 | return nil, errors.New("d must be set") 46 | } 47 | 48 | switch privateKey.CRV { 49 | case SECP256K1JWACurve: 50 | return SECP256K1Sign(payload, privateKey) 51 | default: 52 | return nil, fmt.Errorf("unsupported curve: %s", privateKey.CRV) 53 | } 54 | } 55 | 56 | // Verify verifies the given signature over a given payload by the given public key 57 | // 58 | // # Note 59 | // 60 | // The function will automatically detect the given ECDSA cryptographic curve from the given public key 61 | func Verify(payload []byte, signature []byte, publicKey jwk.JWK) (bool, error) { 62 | switch publicKey.CRV { 63 | case SECP256K1JWACurve: 64 | return SECP256K1Verify(payload, signature, publicKey) 65 | default: 66 | return false, fmt.Errorf("unsupported curve: %s", publicKey.CRV) 67 | } 68 | } 69 | 70 | // GetJWA returns the [JWA] for the given ECDSA key 71 | // 72 | // [JWA]: https://datatracker.ietf.org/doc/html/rfc7518 73 | func GetJWA(jwk jwk.JWK) (string, error) { 74 | switch jwk.CRV { 75 | case SECP256K1JWACurve: 76 | return SECP256K1JWA, nil 77 | default: 78 | return "", fmt.Errorf("unsupported curve: %s", jwk.CRV) 79 | } 80 | } 81 | 82 | // BytesToPublicKey deserializes the given byte array into a jwk.JWK for the given cryptographic algorithm 83 | func BytesToPublicKey(algorithmID string, input []byte) (jwk.JWK, error) { 84 | switch algorithmID { 85 | case SECP256K1AlgorithmID: 86 | return SECP256K1BytesToPublicKey(input) 87 | default: 88 | return jwk.JWK{}, fmt.Errorf("unsupported algorithm: %s", algorithmID) 89 | } 90 | } 91 | 92 | // PublicKeyToBytes serializes the given public key into a byte array 93 | func PublicKeyToBytes(publicKey jwk.JWK) ([]byte, error) { 94 | switch publicKey.CRV { 95 | case SECP256K1JWACurve: 96 | return SECP256K1PublicKeyToBytes(publicKey) 97 | default: 98 | return nil, fmt.Errorf("unsupported curve: %s", publicKey.CRV) 99 | } 100 | } 101 | 102 | // SupportsAlgorithmID informs as to whether or not the given algorithm ID is supported by this package 103 | func SupportsAlgorithmID(id string) bool { 104 | return algorithmIDs[id] 105 | } 106 | 107 | // AlgorithmID returns the algorithm ID for the given jwk.JWK 108 | func AlgorithmID(jwk *jwk.JWK) (string, error) { 109 | switch jwk.CRV { 110 | case SECP256K1JWACurve: 111 | return SECP256K1AlgorithmID, nil 112 | default: 113 | return "", fmt.Errorf("unsupported curve: %s", jwk.CRV) 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /crypto/dsa/ecdsa/secp256k1.go: -------------------------------------------------------------------------------- 1 | package ecdsa 2 | 3 | import ( 4 | "crypto/sha256" 5 | "encoding/base64" 6 | "errors" 7 | "fmt" 8 | 9 | "github.com/decentralized-identity/web5-go/jwk" 10 | _secp256k1 "github.com/decred/dcrd/dcrec/secp256k1/v4" 11 | "github.com/decred/dcrd/dcrec/secp256k1/v4/ecdsa" 12 | ) 13 | 14 | const ( 15 | SECP256K1JWA string = "ES256K" 16 | SECP256K1JWACurve string = "secp256k1" 17 | SECP256K1AlgorithmID string = SECP256K1JWACurve 18 | ) 19 | 20 | // SECP256K1GeneratePrivateKey generates a new private key 21 | func SECP256K1GeneratePrivateKey() (jwk.JWK, error) { 22 | keyPair, err := _secp256k1.GeneratePrivateKey() 23 | if err != nil { 24 | return jwk.JWK{}, fmt.Errorf("failed to generate private key: %w", err) 25 | } 26 | 27 | dBytes := keyPair.Key.Bytes() 28 | pubKey := keyPair.PubKey() 29 | xBytes := pubKey.X().Bytes() 30 | yBytes := pubKey.Y().Bytes() 31 | 32 | privateKey := jwk.JWK{ 33 | KTY: KeyType, 34 | CRV: SECP256K1JWACurve, 35 | D: base64.RawURLEncoding.EncodeToString(dBytes[:]), 36 | X: base64.RawURLEncoding.EncodeToString(xBytes), 37 | Y: base64.RawURLEncoding.EncodeToString(yBytes), 38 | } 39 | 40 | return privateKey, nil 41 | } 42 | 43 | // SECP256K1Sign signs the given payload with the given private key 44 | func SECP256K1Sign(payload []byte, privateKey jwk.JWK) ([]byte, error) { 45 | privateKeyBytes, err := base64.RawURLEncoding.DecodeString(privateKey.D) 46 | if err != nil { 47 | return nil, fmt.Errorf("failed to decode d %w", err) 48 | } 49 | 50 | key := _secp256k1.PrivKeyFromBytes(privateKeyBytes) 51 | 52 | hash := sha256.Sum256(payload) 53 | signature := ecdsa.SignCompact(key, hash[:], false)[1:] 54 | 55 | return signature, nil 56 | } 57 | 58 | // SECP256K1Verify verifies the given signature over the given payload with the given public key 59 | func SECP256K1Verify(payload []byte, signature []byte, publicKey jwk.JWK) (bool, error) { 60 | if publicKey.X == "" || publicKey.Y == "" { 61 | return false, errors.New("x and y must be set") 62 | } 63 | 64 | hash := sha256.Sum256(payload) 65 | 66 | keyBytes, err := secp256k1PublicKeyToUncheckedBytes(publicKey) 67 | if err != nil { 68 | return false, fmt.Errorf("failed to convert public key to bytes: %w", err) 69 | } 70 | 71 | key, err := _secp256k1.ParsePubKey(keyBytes) 72 | if err != nil { 73 | return false, fmt.Errorf("failed to parse public key: %w", err) 74 | } 75 | 76 | if len(signature) != 64 { 77 | return false, errors.New("signature must be 64 bytes") 78 | } 79 | 80 | r := new(_secp256k1.ModNScalar) 81 | r.SetByteSlice(signature[:32]) 82 | 83 | s := new(_secp256k1.ModNScalar) 84 | s.SetByteSlice(signature[32:]) 85 | 86 | sig := ecdsa.NewSignature(r, s) 87 | legit := sig.Verify(hash[:], key) 88 | 89 | return legit, nil 90 | } 91 | 92 | // SECP256K1BytesToPublicKey converts a secp256k1 public key to a JWK. 93 | // Supports both Compressed and Uncompressed public keys described in 94 | // https://www.secg.org/sec1-v2.pdf section 2.3.3 95 | func SECP256K1BytesToPublicKey(input []byte) (jwk.JWK, error) { 96 | pubKey, err := _secp256k1.ParsePubKey(input) 97 | if err != nil { 98 | return jwk.JWK{}, fmt.Errorf("failed to parse public key: %w", err) 99 | } 100 | 101 | return jwk.JWK{ 102 | KTY: KeyType, 103 | CRV: SECP256K1JWACurve, 104 | X: base64.RawURLEncoding.EncodeToString(pubKey.X().Bytes()), 105 | Y: base64.RawURLEncoding.EncodeToString(pubKey.Y().Bytes()), 106 | }, nil 107 | } 108 | 109 | // SECP256K1PublicKeyToBytes converts a secp256k1 public key JWK to bytes. 110 | // Note: this function returns the uncompressed public key. compressed is not 111 | // yet supported 112 | func SECP256K1PublicKeyToBytes(publicKey jwk.JWK) ([]byte, error) { 113 | uncheckedBytes, err := secp256k1PublicKeyToUncheckedBytes(publicKey) 114 | if err != nil { 115 | return nil, err 116 | } 117 | 118 | key, err := _secp256k1.ParsePubKey(uncheckedBytes) 119 | if err != nil { 120 | return nil, fmt.Errorf("invalid public key: %w", err) 121 | } 122 | 123 | return key.SerializeUncompressed(), nil 124 | } 125 | 126 | func secp256k1PublicKeyToUncheckedBytes(publicKey jwk.JWK) ([]byte, error) { 127 | if publicKey.X == "" || publicKey.Y == "" { 128 | return nil, errors.New("x and y must be set") 129 | } 130 | 131 | x, err := base64.RawURLEncoding.DecodeString(publicKey.X) 132 | if err != nil { 133 | return nil, fmt.Errorf("failed to decode x: %w", err) 134 | } 135 | 136 | y, err := base64.RawURLEncoding.DecodeString(publicKey.Y) 137 | if err != nil { 138 | return nil, fmt.Errorf("failed to decode y: %w", err) 139 | } 140 | 141 | // Prepend 0x04 to indicate an uncompressed public key format for secp256k1. 142 | // This byte is a prefix that distinguishes uncompressed keys, which include both X and Y coordinates, 143 | // from compressed keys which only include one coordinate and an indication of the other's parity. 144 | // The secp256k1 standard requires this prefix for uncompressed keys to ensure proper interpretation. 145 | keyBytes := []byte{0x04} 146 | keyBytes = append(keyBytes, x...) 147 | keyBytes = append(keyBytes, y...) 148 | 149 | return keyBytes, nil 150 | } 151 | -------------------------------------------------------------------------------- /crypto/dsa/ecdsa/secp256k1_test.go: -------------------------------------------------------------------------------- 1 | package ecdsa_test 2 | 3 | import ( 4 | "encoding/hex" 5 | "testing" 6 | 7 | "github.com/alecthomas/assert/v2" 8 | "github.com/decentralized-identity/web5-go/crypto/dsa/ecdsa" 9 | "github.com/decentralized-identity/web5-go/jwk" 10 | ) 11 | 12 | func TestSECP256K1GeneratePrivateKey(t *testing.T) { 13 | key, err := ecdsa.SECP256K1GeneratePrivateKey() 14 | assert.NoError(t, err) 15 | 16 | assert.Equal(t, ecdsa.KeyType, key.KTY) 17 | assert.Equal(t, ecdsa.SECP256K1JWACurve, key.CRV) 18 | assert.True(t, key.D != "", "privateJwk.D is empty") 19 | assert.True(t, key.X != "", "privateJwk.X is empty") 20 | assert.True(t, key.Y != "", "privateJwk.Y is empty") 21 | } 22 | 23 | func TestSECP256K1BytesToPublicKey_Bad(t *testing.T) { 24 | _, err := ecdsa.SECP256K1BytesToPublicKey([]byte{0x00, 0x01, 0x02, 0x03}) 25 | assert.Error(t, err) 26 | } 27 | 28 | func TestSECP256K1BytesToPublicKey_Uncompressed(t *testing.T) { 29 | // vector taken from https://github.com/decentralized-identity/web5-js/blob/dids-new-crypto/packages/crypto/tests/fixtures/test-vectors/secp256k1/bytes-to-public-key.json 30 | publicKeyHex := "0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8" 31 | pubKeyBytes, err := hex.DecodeString(publicKeyHex) 32 | assert.NoError(t, err) 33 | 34 | jwk, err := ecdsa.SECP256K1BytesToPublicKey(pubKeyBytes) 35 | assert.NoError(t, err) 36 | 37 | assert.Equal(t, ecdsa.SECP256K1JWACurve, jwk.CRV) 38 | assert.Equal(t, ecdsa.KeyType, jwk.KTY) 39 | assert.Equal(t, "eb5mfvncu6xVoGKVzocLBwKb_NstzijZWfKBWxb4F5g", jwk.X) 40 | assert.Equal(t, "SDradyajxGVdpPv8DhEIqP0XtEimhVQZnEfQj_sQ1Lg", jwk.Y) 41 | } 42 | 43 | func TestSECP256K1PublicKeyToBytes(t *testing.T) { 44 | // vector taken from https://github.com/decentralized-identity/web5-js/blob/dids-new-crypto/packages/crypto/tests/fixtures/test-vectors/secp256k1/bytes-to-public-key.json 45 | jwk := jwk.JWK{ 46 | KTY: "EC", 47 | CRV: ecdsa.SECP256K1JWACurve, 48 | X: "eb5mfvncu6xVoGKVzocLBwKb_NstzijZWfKBWxb4F5g", 49 | Y: "SDradyajxGVdpPv8DhEIqP0XtEimhVQZnEfQj_sQ1Lg", 50 | } 51 | 52 | pubKeyBytes, err := ecdsa.SECP256K1PublicKeyToBytes(jwk) 53 | assert.NoError(t, err) 54 | 55 | pubKeyHex := hex.EncodeToString(pubKeyBytes) 56 | expected := "0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8" 57 | 58 | assert.Equal(t, expected, pubKeyHex) 59 | } 60 | 61 | func TestSECP256K1PublicKeyToBytes_Bad(t *testing.T) { 62 | vectors := []jwk.JWK{ 63 | { 64 | KTY: "EC", 65 | CRV: ecdsa.SECP256K1JWACurve, 66 | X: "eb5mfvncu6xVoGKVzocLBwKb_NstzijZWfKBWxb4F5g", 67 | }, 68 | { 69 | KTY: "EC", 70 | CRV: ecdsa.SECP256K1JWACurve, 71 | Y: "eb5mfvncu6xVoGKVzocLBwKb_NstzijZWfKBWxb4F5g", 72 | }, 73 | { 74 | KTY: "EC", 75 | CRV: ecdsa.SECP256K1JWACurve, 76 | X: "=///", 77 | Y: "SDradyajxGVdpPv8DhEIqP0XtEimhVQZnEfQj_sQ1Lg", 78 | }, 79 | { 80 | KTY: "EC", 81 | CRV: ecdsa.SECP256K1JWACurve, 82 | X: "eb5mfvncu6xVoGKVzocLBwKb_NstzijZWfKBWxb4F5g", 83 | Y: "=///", 84 | }, 85 | { 86 | KTY: "EC", 87 | CRV: ecdsa.SECP256K1JWACurve, 88 | X: "eb5mfvncu6xVoGKVzocLBwKb_NstzijZWfKBWxb4F5g", 89 | Y: "SDradyajxGVdpPv8DhEIqP0XtEimhVQZnEfQj_sQ1Lg2", 90 | }, 91 | } 92 | 93 | for _, vec := range vectors { 94 | pubKeyBytes, err := ecdsa.SECP256K1PublicKeyToBytes(vec) 95 | assert.Error(t, err) 96 | assert.Equal(t, nil, pubKeyBytes) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /crypto/dsa/eddsa/ed25519.go: -------------------------------------------------------------------------------- 1 | package eddsa 2 | 3 | import ( 4 | _ed25519 "crypto/ed25519" 5 | "crypto/rand" 6 | "encoding/base64" 7 | "errors" 8 | "fmt" 9 | 10 | "github.com/decentralized-identity/web5-go/jwk" 11 | ) 12 | 13 | const ( 14 | ED25519JWACurve string = "Ed25519" 15 | ED25519AlgorithmID string = ED25519JWACurve 16 | ) 17 | 18 | // ED25519GeneratePrivateKey generates a new ED25519 private key 19 | func ED25519GeneratePrivateKey() (jwk.JWK, error) { 20 | publicKey, privateKey, err := _ed25519.GenerateKey(rand.Reader) 21 | if err != nil { 22 | return jwk.JWK{}, err 23 | } 24 | 25 | privKeyJwk := jwk.JWK{ 26 | KTY: KeyType, 27 | CRV: ED25519JWACurve, 28 | D: base64.RawURLEncoding.EncodeToString(privateKey), 29 | X: base64.RawURLEncoding.EncodeToString(publicKey), 30 | } 31 | 32 | return privKeyJwk, nil 33 | } 34 | 35 | // ED25519Sign signs the given payload with the given private key 36 | func ED25519Sign(payload []byte, privateKey jwk.JWK) ([]byte, error) { 37 | privateKeyBytes, err := base64.RawURLEncoding.DecodeString(privateKey.D) 38 | if err != nil { 39 | return nil, fmt.Errorf("failed to decode d %w", err) 40 | } 41 | 42 | signature := _ed25519.Sign(privateKeyBytes, payload) 43 | return signature, nil 44 | } 45 | 46 | // ED25519Verify verifies the given signature against the given payload using the given public key 47 | func ED25519Verify(payload []byte, signature []byte, publicKey jwk.JWK) (bool, error) { 48 | publicKeyBytes, err := base64.RawURLEncoding.DecodeString(publicKey.X) 49 | if err != nil { 50 | return false, err 51 | } 52 | 53 | legit := _ed25519.Verify(publicKeyBytes, payload, signature) 54 | return legit, nil 55 | } 56 | 57 | // ED25519BytesToPublicKey deserializes the byte array into a jwk.JWK public key 58 | func ED25519BytesToPublicKey(input []byte) (jwk.JWK, error) { 59 | if len(input) != _ed25519.PublicKeySize { 60 | return jwk.JWK{}, errors.New("invalid public key") 61 | } 62 | 63 | return jwk.JWK{ 64 | KTY: KeyType, 65 | CRV: ED25519JWACurve, 66 | X: base64.RawURLEncoding.EncodeToString(input), 67 | }, nil 68 | } 69 | 70 | // ED25519PublicKeyToBytes serializes the given public key int a byte array 71 | func ED25519PublicKeyToBytes(publicKey jwk.JWK) ([]byte, error) { 72 | if publicKey.X == "" { 73 | return nil, errors.New("x must be set") 74 | } 75 | 76 | publicKeyBytes, err := base64.RawURLEncoding.DecodeString(publicKey.X) 77 | if err != nil { 78 | return nil, fmt.Errorf("failed to decode x %w", err) 79 | } 80 | 81 | return publicKeyBytes, nil 82 | } 83 | -------------------------------------------------------------------------------- /crypto/dsa/eddsa/ed25519_test.go: -------------------------------------------------------------------------------- 1 | package eddsa_test 2 | 3 | import ( 4 | "encoding/base64" 5 | "encoding/hex" 6 | "testing" 7 | 8 | "github.com/alecthomas/assert/v2" 9 | "github.com/decentralized-identity/web5-go/crypto/dsa/eddsa" 10 | "github.com/decentralized-identity/web5-go/jwk" 11 | ) 12 | 13 | func TestED25519BytesToPublicKey_Bad(t *testing.T) { 14 | publicKeyBytes := []byte{0x00, 0x01, 0x02, 0x03} 15 | _, err := eddsa.ED25519BytesToPublicKey(publicKeyBytes) 16 | assert.Error(t, err) 17 | } 18 | 19 | func TestED25519BytesToPublicKey_Good(t *testing.T) { 20 | // vector taken from https://github.com/decentralized-identity/web5-js/blob/dids-new-crypto/packages/crypto/tests/fixtures/test-vectors/ed25519/bytes-to-public-key.json 21 | pubKeyHex := "7d4d0e7f6153a69b6242b522abbee685fda4420f8834b108c3bdae369ef549fa" 22 | pubKeyBytes, err := hex.DecodeString(pubKeyHex) 23 | assert.NoError(t, err) 24 | 25 | jwk, err := eddsa.ED25519BytesToPublicKey(pubKeyBytes) 26 | assert.NoError(t, err) 27 | 28 | assert.Equal(t, eddsa.KeyType, jwk.KTY) 29 | assert.Equal(t, eddsa.ED25519JWACurve, jwk.CRV) 30 | assert.Equal(t, "fU0Of2FTpptiQrUiq77mhf2kQg-INLEIw72uNp71Sfo", jwk.X) 31 | } 32 | 33 | func TestED25519PublicKeyToBytes(t *testing.T) { 34 | // vector taken from: https://github.com/decentralized-identity/web5-spec/blob/main/test-vectors/crypto_ed25519/sign.json 35 | jwk := jwk.JWK{ 36 | KTY: "OKP", 37 | CRV: eddsa.ED25519JWACurve, 38 | X: "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo", 39 | } 40 | 41 | pubKeyBytes, err := eddsa.ED25519PublicKeyToBytes(jwk) 42 | assert.NoError(t, err) 43 | 44 | pubKeyB64URL := base64.RawURLEncoding.EncodeToString(pubKeyBytes) 45 | assert.Equal(t, jwk.X, pubKeyB64URL) 46 | } 47 | 48 | func TestED25519PublicKeyToBytes_Bad(t *testing.T) { 49 | vectors := []jwk.JWK{ 50 | { 51 | KTY: "OKP", 52 | CRV: eddsa.ED25519JWACurve, 53 | }, 54 | { 55 | KTY: "OKP", 56 | CRV: eddsa.ED25519JWACurve, 57 | X: "=/---", 58 | }, 59 | } 60 | 61 | for _, jwk := range vectors { 62 | pubKeyBytes, err := eddsa.ED25519PublicKeyToBytes(jwk) 63 | assert.Error(t, err) 64 | 65 | assert.Equal(t, nil, pubKeyBytes) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /crypto/dsa/eddsa/eddsa.go: -------------------------------------------------------------------------------- 1 | // Package eddsa implements the EdDSA signature schemes as per RFC 8032 2 | // https://tools.ietf.org/html/rfc8032. Note: Currently only Ed25519 is supported 3 | package eddsa 4 | 5 | import ( 6 | "errors" 7 | "fmt" 8 | 9 | "github.com/decentralized-identity/web5-go/jwk" 10 | ) 11 | 12 | const ( 13 | JWA string = "EdDSA" 14 | KeyType string = "OKP" 15 | ) 16 | 17 | var algorithmIDs = map[string]bool{ 18 | ED25519AlgorithmID: true, 19 | } 20 | 21 | // GeneratePrivateKey generates an EdDSA private key for the given algorithm 22 | func GeneratePrivateKey(algorithmID string) (jwk.JWK, error) { 23 | switch algorithmID { 24 | case ED25519AlgorithmID: 25 | return ED25519GeneratePrivateKey() 26 | default: 27 | return jwk.JWK{}, fmt.Errorf("unsupported algorithm: %s", algorithmID) 28 | } 29 | } 30 | 31 | // GetPublicKey builds an EdDSA public key from the given EdDSA private key 32 | func GetPublicKey(privateKey jwk.JWK) jwk.JWK { 33 | return jwk.JWK{ 34 | KTY: privateKey.KTY, 35 | CRV: privateKey.CRV, 36 | X: privateKey.X, 37 | } 38 | } 39 | 40 | // Sign generates a cryptographic signature for the given payload with the given private key 41 | // 42 | // # Note 43 | // 44 | // The function will automatically detect the given EdDSA cryptographic curve from the given private key 45 | func Sign(payload []byte, privateKey jwk.JWK) ([]byte, error) { 46 | if privateKey.D == "" { 47 | return nil, errors.New("d must be set") 48 | } 49 | 50 | switch privateKey.CRV { 51 | case ED25519JWACurve: 52 | return ED25519Sign(payload, privateKey) 53 | default: 54 | return nil, fmt.Errorf("unsupported curve: %s", privateKey.CRV) 55 | } 56 | } 57 | 58 | // Verify verifies the given signature over a given payload by the given public key 59 | // 60 | // # Note 61 | // 62 | // The function will automatically detect the given EdDSA cryptographic curve from the given public key 63 | func Verify(payload []byte, signature []byte, publicKey jwk.JWK) (bool, error) { 64 | switch publicKey.CRV { 65 | case ED25519JWACurve: 66 | return ED25519Verify(payload, signature, publicKey) 67 | default: 68 | return false, fmt.Errorf("unsupported curve: %s", publicKey.CRV) 69 | } 70 | } 71 | 72 | // GetJWA returns the [JWA] for the given EdDSA key 73 | // 74 | // # Note 75 | // 76 | // The only supported [JWA] is "EdDSA" 77 | // 78 | // [JWA]: https://datatracker.ietf.org/doc/html/rfc7518 79 | func GetJWA(jwk jwk.JWK) (string, error) { 80 | return JWA, nil 81 | } 82 | 83 | // BytesToPublicKey deserializes the given byte array into a jwk.JWK for the given cryptographic algorithm 84 | func BytesToPublicKey(algorithmID string, input []byte) (jwk.JWK, error) { 85 | switch algorithmID { 86 | case ED25519AlgorithmID: 87 | return ED25519BytesToPublicKey(input) 88 | default: 89 | return jwk.JWK{}, fmt.Errorf("unsupported algorithm: %s", algorithmID) 90 | } 91 | } 92 | 93 | // PublicKeyToBytes serializes the given public key into a byte array 94 | func PublicKeyToBytes(publicKey jwk.JWK) ([]byte, error) { 95 | switch publicKey.CRV { 96 | case ED25519JWACurve: 97 | return ED25519PublicKeyToBytes(publicKey) 98 | default: 99 | return nil, fmt.Errorf("unsupported curve: %s", publicKey.CRV) 100 | } 101 | } 102 | 103 | // SupportsAlgorithmID informs as to whether or not the given algorithm ID is supported by this package 104 | func SupportsAlgorithmID(id string) bool { 105 | return algorithmIDs[id] 106 | } 107 | 108 | // AlgorithmID returns the algorithm ID for the given jwk.JWK 109 | func AlgorithmID(jwk *jwk.JWK) (string, error) { 110 | switch jwk.CRV { 111 | case ED25519JWACurve: 112 | return ED25519AlgorithmID, nil 113 | default: 114 | return "", fmt.Errorf("unsupported curve: %s", jwk.CRV) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /crypto/entropy.go: -------------------------------------------------------------------------------- 1 | package crypto 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/hex" 6 | "errors" 7 | ) 8 | 9 | // EntropySize represents the size of the entropy in bits, i.e. Entropy128 is equal to 128 bits (or 16 bytes) of entrop 10 | type EntropySize int 11 | 12 | // Directly set the sizes according to NIST recommendations for entropy 13 | // defined here: https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-90Ar1.pdf 14 | const ( 15 | Entropy112 EntropySize = 112 / 8 // 14 bytes 16 | Entropy128 EntropySize = 128 / 8 // 16 bytes 17 | Entropy192 EntropySize = 192 / 8 // 24 bytes 18 | Entropy256 EntropySize = 256 / 8 // 32 bytes 19 | ) 20 | 21 | // GenerateEntropy generates a random byte array of size n bytes 22 | func GenerateEntropy(n EntropySize) ([]byte, error) { 23 | if n <= 0 { 24 | return nil, errors.New("entropy byte size must be > 0") 25 | } 26 | 27 | bytes := make([]byte, n) 28 | _, err := rand.Read(bytes) 29 | if err != nil { 30 | return nil, err 31 | } 32 | 33 | return bytes, nil 34 | } 35 | 36 | // GenerateNonce generates a hex-encoded nonce by calling GenerateEntropy with a size of 16 bytes (128 bits) 37 | func GenerateNonce(n EntropySize) (string, error) { 38 | bytes, err := GenerateEntropy(n) 39 | if err != nil { 40 | return "", err 41 | } 42 | 43 | return hex.EncodeToString(bytes), nil 44 | } 45 | -------------------------------------------------------------------------------- /crypto/entropy_test.go: -------------------------------------------------------------------------------- 1 | package crypto_test 2 | 3 | import ( 4 | "encoding/hex" 5 | "testing" 6 | 7 | "github.com/alecthomas/assert/v2" 8 | "github.com/decentralized-identity/web5-go/crypto" 9 | ) 10 | 11 | func Test_GenerateEntropy(t *testing.T) { 12 | bytes, err := crypto.GenerateEntropy(crypto.Entropy128) 13 | assert.NoError(t, err) 14 | assert.Equal(t, int(crypto.Entropy128), len(bytes)) 15 | } 16 | 17 | func Test_GenerateEntropy_CustomSize(t *testing.T) { 18 | customSize := 99 19 | bytes, err := crypto.GenerateEntropy(crypto.EntropySize(customSize)) 20 | assert.NoError(t, err) 21 | assert.Equal(t, customSize, len(bytes)) 22 | } 23 | 24 | func Test_GenerateEntropy_InvalidSize(t *testing.T) { 25 | bytes, err := crypto.GenerateEntropy(0) 26 | assert.Error(t, err) 27 | assert.Equal(t, nil, bytes) 28 | 29 | bytes, err = crypto.GenerateEntropy(-1) 30 | assert.Error(t, err) 31 | assert.Equal(t, nil, bytes) 32 | } 33 | 34 | func Test_GenerateNonce(t *testing.T) { 35 | nonce, err := crypto.GenerateNonce(crypto.Entropy128) 36 | assert.NoError(t, err) 37 | assert.Equal(t, int(crypto.Entropy128)*2, len(nonce)) 38 | 39 | _, err = hex.DecodeString(nonce) 40 | assert.NoError(t, err) 41 | } 42 | 43 | func Test_GenerateNonce_CustomSize(t *testing.T) { 44 | customSize := 99 45 | nonce, err := crypto.GenerateNonce(crypto.EntropySize(99)) 46 | assert.NoError(t, err) 47 | assert.Equal(t, customSize*2, len(nonce)) 48 | 49 | _, err = hex.DecodeString(nonce) 50 | assert.NoError(t, err) 51 | } 52 | 53 | func Test_GenerateNonce_InvalidSize(t *testing.T) { 54 | nonce, err := crypto.GenerateNonce(0) 55 | assert.Error(t, err) 56 | assert.Equal(t, "", nonce) 57 | } 58 | -------------------------------------------------------------------------------- /crypto/keymanager.go: -------------------------------------------------------------------------------- 1 | package crypto 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/decentralized-identity/web5-go/crypto/dsa" 7 | "github.com/decentralized-identity/web5-go/jwk" 8 | ) 9 | 10 | // KeyManager is an abstraction that can be leveraged to manage/use keys (create, sign etc) as desired per the given use case 11 | // examples of concrete implementations include: AWS KMS, Azure Key Vault, Google Cloud KMS, Hashicorp Vault etc 12 | type KeyManager interface { 13 | // GeneratePrivateKey generates a new private key, stores it in the key store and returns the key id 14 | GeneratePrivateKey(algorithmID string) (string, error) 15 | 16 | // GetPublicKey returns the public key for the given key id 17 | GetPublicKey(keyID string) (jwk.JWK, error) 18 | 19 | // Sign signs the given payload with the private key for the given key id 20 | Sign(keyID string, payload []byte) ([]byte, error) 21 | } 22 | 23 | // KeyExporter is an abstraction that can be leveraged to implement types which intend to export keys 24 | type KeyExporter interface { 25 | ExportKey(keyID string) (jwk.JWK, error) 26 | } 27 | 28 | // KeyImporter is an abstraction that can be leveraged to implement types which intend to import keys 29 | type KeyImporter interface { 30 | ImportKey(key jwk.JWK) (string, error) 31 | } 32 | 33 | // LocalKeyManager is an implementation of KeyManager that stores keys in memory 34 | type LocalKeyManager struct { 35 | keys map[string]jwk.JWK 36 | } 37 | 38 | // NewLocalKeyManager returns a new instance of InMemoryKeyManager 39 | func NewLocalKeyManager() *LocalKeyManager { 40 | return &LocalKeyManager{ 41 | keys: make(map[string]jwk.JWK), 42 | } 43 | } 44 | 45 | // GeneratePrivateKey generates a new private key using the algorithm provided, 46 | // stores it in the key store and returns the key id 47 | // Supported algorithms are available in [github.com/decentralized-identity/web5-go/crypto/dsa.AlgorithmID] 48 | func (k *LocalKeyManager) GeneratePrivateKey(algorithmID string) (string, error) { 49 | var keyAlias string 50 | 51 | key, err := dsa.GeneratePrivateKey(algorithmID) 52 | if err != nil { 53 | return "", fmt.Errorf("failed to generate private key: %w", err) 54 | } 55 | 56 | keyAlias, err = key.ComputeThumbprint() 57 | if err != nil { 58 | return "", fmt.Errorf("failed to compute key alias: %w", err) 59 | } 60 | 61 | k.keys[keyAlias] = key 62 | 63 | return keyAlias, nil 64 | } 65 | 66 | // GetPublicKey returns the public key for the given key id 67 | func (k *LocalKeyManager) GetPublicKey(keyID string) (jwk.JWK, error) { 68 | key, err := k.getPrivateJWK(keyID) 69 | if err != nil { 70 | return jwk.JWK{}, err 71 | } 72 | 73 | return dsa.GetPublicKey(key), nil 74 | 75 | } 76 | 77 | // Sign signs the payload with the private key for the given key id 78 | func (k *LocalKeyManager) Sign(keyID string, payload []byte) ([]byte, error) { 79 | key, err := k.getPrivateJWK(keyID) 80 | if err != nil { 81 | return nil, err 82 | } 83 | 84 | return dsa.Sign(payload, key) 85 | } 86 | 87 | func (k *LocalKeyManager) getPrivateJWK(keyID string) (jwk.JWK, error) { 88 | key, ok := k.keys[keyID] 89 | 90 | if !ok { 91 | return jwk.JWK{}, fmt.Errorf("key with alias %s not found", keyID) 92 | } 93 | 94 | return key, nil 95 | } 96 | 97 | // ExportKey exports the key specific by the key ID from the [LocalKeyManager] 98 | func (k *LocalKeyManager) ExportKey(keyID string) (jwk.JWK, error) { 99 | key, err := k.getPrivateJWK(keyID) 100 | if err != nil { 101 | return jwk.JWK{}, err 102 | } 103 | 104 | return key, nil 105 | } 106 | 107 | // ImportKey imports the key into the [LocalKeyManager] and returns the key alias 108 | func (k *LocalKeyManager) ImportKey(key jwk.JWK) (string, error) { 109 | keyAlias, err := key.ComputeThumbprint() 110 | if err != nil { 111 | return "", fmt.Errorf("failed to compute key alias: %w", err) 112 | } 113 | 114 | k.keys[keyAlias] = key 115 | 116 | return keyAlias, nil 117 | } 118 | -------------------------------------------------------------------------------- /crypto/keymanager_test.go: -------------------------------------------------------------------------------- 1 | package crypto_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/alecthomas/assert/v2" 7 | "github.com/decentralized-identity/web5-go/crypto" 8 | "github.com/decentralized-identity/web5-go/crypto/dsa" 9 | ) 10 | 11 | func TestGeneratePrivateKey(t *testing.T) { 12 | keyManager := crypto.NewLocalKeyManager() 13 | 14 | keyID, err := keyManager.GeneratePrivateKey(dsa.AlgorithmIDSECP256K1) 15 | assert.NoError(t, err) 16 | 17 | assert.True(t, keyID != "", "keyID is empty") 18 | } 19 | 20 | func TestGetPublicKey(t *testing.T) { 21 | keyManager := crypto.NewLocalKeyManager() 22 | 23 | keyID, err := keyManager.GeneratePrivateKey(dsa.AlgorithmIDSECP256K1) 24 | assert.NoError(t, err) 25 | 26 | publicKey, err := keyManager.GetPublicKey(keyID) 27 | assert.NoError(t, err) 28 | 29 | thumbprint, err := publicKey.ComputeThumbprint() 30 | assert.NoError(t, err) 31 | 32 | assert.Equal[string](t, keyID, thumbprint, "unexpected keyID") 33 | } 34 | 35 | func TestSign(t *testing.T) { 36 | keyManager := crypto.NewLocalKeyManager() 37 | 38 | keyID, err := keyManager.GeneratePrivateKey(dsa.AlgorithmIDSECP256K1) 39 | assert.NoError(t, err) 40 | 41 | payload := []byte("hello world") 42 | signature, err := keyManager.Sign(keyID, payload) 43 | assert.NoError(t, err) 44 | 45 | if signature == nil { 46 | t.Errorf("signature is nil") 47 | } 48 | 49 | assert.True(t, signature != nil, "signature is nil") 50 | } 51 | -------------------------------------------------------------------------------- /diagrams/dids-pkg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/decentralized-identity/web5-go/473f33eff0d26edd4b98875155bdcd944025d3ba/diagrams/dids-pkg.png -------------------------------------------------------------------------------- /dids/did/bearerdid.go: -------------------------------------------------------------------------------- 1 | package did 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/decentralized-identity/web5-go/crypto" 7 | "github.com/decentralized-identity/web5-go/dids/didcore" 8 | "github.com/decentralized-identity/web5-go/jwk" 9 | ) 10 | 11 | // BearerDID is a composite type that combines a DID with a KeyManager containing keys 12 | // associated to the DID. Together, these two components form a BearerDID that can be used to 13 | // sign and verify data. 14 | type BearerDID struct { 15 | DID 16 | crypto.KeyManager 17 | Document didcore.Document 18 | } 19 | 20 | // DIDSigner is a function returned by GetSigner that can be used to sign a payload with a key 21 | // associated to a BearerDID. 22 | type DIDSigner func(payload []byte) ([]byte, error) 23 | 24 | // ToPortableDID exports a BearerDID to a portable format 25 | func (d *BearerDID) ToPortableDID() (PortableDID, error) { 26 | portableDID := PortableDID{ 27 | URI: d.URI, 28 | Document: d.Document, 29 | } 30 | 31 | exporter, ok := d.KeyManager.(crypto.KeyExporter) 32 | if ok { 33 | privateKeys := make([]jwk.JWK, 0) 34 | 35 | for _, vm := range d.Document.VerificationMethod { 36 | keyAlias, err := vm.PublicKeyJwk.ComputeThumbprint() 37 | if err != nil { 38 | continue 39 | } 40 | 41 | key, err := exporter.ExportKey(keyAlias) 42 | if err != nil { 43 | // TODO: decide if we want to blow up or continue 44 | continue 45 | } 46 | 47 | privateKeys = append(privateKeys, key) 48 | } 49 | 50 | portableDID.PrivateKeys = privateKeys 51 | } 52 | 53 | return portableDID, nil 54 | } 55 | 56 | // GetSigner returns a sign method that can be used to sign a payload using a key associated to the DID. 57 | // This function also returns the verification method needed to verify the signature. 58 | // 59 | // Providing the verification method allows the caller to provide the signature's recipient 60 | // with a reference to the verification method needed to verify the payload. This is often done 61 | // by including the verification method id either alongside the signature or as part of the header 62 | // in the case of JSON Web Signatures. 63 | // 64 | // The verifier can dereference the verification method id to obtain the public key needed to verify the signature. 65 | // 66 | // This function takes a Verification Method selector that can be used to select a specific verification method 67 | // from the DID Document if desired. If no selector is provided, the payload will be signed with the key associated 68 | // to the first verification method in the DID Document. 69 | // 70 | // The selector can either be a Verification Method ID or a Purpose. If a Purpose is provided, the first verification 71 | // method in the DID Document that has the provided purpose will be used to sign the payload. 72 | // 73 | // The returned signer is a function that takes a byte payload and returns a byte signature. 74 | func (d *BearerDID) GetSigner(selector didcore.VMSelector) (DIDSigner, didcore.VerificationMethod, error) { 75 | vm, err := d.Document.SelectVerificationMethod(selector) 76 | if err != nil { 77 | return nil, didcore.VerificationMethod{}, err 78 | } 79 | 80 | keyAlias, err := vm.PublicKeyJwk.ComputeThumbprint() 81 | if err != nil { 82 | return nil, didcore.VerificationMethod{}, fmt.Errorf("failed to compute key alias: %s", err.Error()) 83 | } 84 | 85 | signer := func(payload []byte) ([]byte, error) { 86 | return d.Sign(keyAlias, payload) 87 | } 88 | 89 | return signer, vm, nil 90 | } 91 | 92 | // FromPortableDID inflates a BearerDID from a portable format. 93 | func FromPortableDID(portableDID PortableDID) (BearerDID, error) { 94 | did, err := Parse(portableDID.URI) 95 | if err != nil { 96 | return BearerDID{}, err 97 | } 98 | 99 | keyManager := crypto.NewLocalKeyManager() 100 | for _, key := range portableDID.PrivateKeys { 101 | _, err := keyManager.ImportKey(key) 102 | if err != nil { 103 | // todo what should we do here? 104 | return BearerDID{}, err 105 | } 106 | } 107 | 108 | return BearerDID{ 109 | DID: did, 110 | KeyManager: keyManager, 111 | Document: portableDID.Document, 112 | }, nil 113 | } 114 | -------------------------------------------------------------------------------- /dids/did/bearerdid_test.go: -------------------------------------------------------------------------------- 1 | package did_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/alecthomas/assert/v2" 7 | "github.com/decentralized-identity/web5-go/crypto/dsa" 8 | "github.com/decentralized-identity/web5-go/dids/did" 9 | "github.com/decentralized-identity/web5-go/dids/didcore" 10 | "github.com/decentralized-identity/web5-go/dids/didjwk" 11 | "github.com/decentralized-identity/web5-go/jwk" 12 | "github.com/decentralized-identity/web5-go/jws" 13 | ) 14 | 15 | func TestToPortableDID(t *testing.T) { 16 | did, err := didjwk.Create() 17 | assert.NoError(t, err) 18 | 19 | portableDID, err := did.ToPortableDID() 20 | assert.NoError(t, err) 21 | 22 | assert.Equal[string](t, did.URI, portableDID.URI) 23 | assert.True(t, len(portableDID.PrivateKeys) == 1, "expected 1 key") 24 | 25 | key := portableDID.PrivateKeys[0] 26 | 27 | assert.NotEqual(t, jwk.JWK{}, key, "expected key to not be empty") 28 | } 29 | 30 | func TestFromPortableDID(t *testing.T) { 31 | bearerDID, err := didjwk.Create() 32 | assert.NoError(t, err) 33 | 34 | portableDID, err := bearerDID.ToPortableDID() 35 | assert.NoError(t, err) 36 | 37 | importedDID, err := did.FromPortableDID(portableDID) 38 | assert.NoError(t, err) 39 | 40 | payload := []byte("hi") 41 | 42 | compactJWS, err := jws.Sign(payload, bearerDID) 43 | assert.NoError(t, err) 44 | 45 | compactJWSAgane, err := jws.Sign(payload, importedDID) 46 | assert.NoError(t, err) 47 | 48 | assert.Equal[string](t, compactJWS, compactJWSAgane, "failed to produce same signature with imported did") 49 | } 50 | 51 | func TestGetSigner(t *testing.T) { 52 | bearerDID, err := didjwk.Create() 53 | assert.NoError(t, err) 54 | 55 | sign, vm, err := bearerDID.GetSigner(nil) 56 | assert.NoError(t, err) 57 | 58 | assert.NotEqual(t, vm, didcore.VerificationMethod{}, "expected verification method to not be empty") 59 | 60 | payload := []byte("hi") 61 | signature, err := sign(payload) 62 | assert.NoError(t, err) 63 | 64 | legit, err := dsa.Verify(payload, signature, *vm.PublicKeyJwk) 65 | assert.NoError(t, err) 66 | 67 | assert.True(t, legit, "expected signature to be valid") 68 | } 69 | -------------------------------------------------------------------------------- /dids/did/did.go: -------------------------------------------------------------------------------- 1 | package did 2 | 3 | import ( 4 | "database/sql/driver" 5 | "errors" 6 | "fmt" 7 | "regexp" 8 | "strings" 9 | ) 10 | 11 | // DID provides a way to parse and handle Decentralized Identifier (DID) URIs 12 | // according to the W3C DID Core specification (https://www.w3.org/TR/did-core/). 13 | type DID struct { 14 | // URI represents the complete Decentralized Identifier (DID) URI. 15 | // Spec: https://www.w3.org/TR/did-core/#did-syntax 16 | URI string 17 | 18 | // Method specifies the DID method in the URI, which indicates the underlying 19 | // method-specific identifier scheme (e.g., jwk, dht, key, etc.). 20 | // Spec: https://www.w3.org/TR/did-core/#method-schemes 21 | Method string 22 | 23 | // ID is the method-specific identifier in the DID URI. 24 | // Spec: https://www.w3.org/TR/did-core/#method-specific-id 25 | ID string 26 | 27 | // Params is a map containing optional parameters present in the DID URI. 28 | // These parameters are method-specific. 29 | // Spec: https://www.w3.org/TR/did-core/#did-parameters 30 | Params map[string]string 31 | 32 | // Path is an optional path component in the DID URI. 33 | // Spec: https://www.w3.org/TR/did-core/#path 34 | Path string 35 | 36 | // Query is an optional query component in the DID URI, used to express a request 37 | // for a specific representation or resource related to the DID. 38 | // Spec: https://www.w3.org/TR/did-core/#query 39 | Query string 40 | 41 | // Fragment is an optional fragment component in the DID URI, used to reference 42 | // a specific part of a DID document. 43 | // Spec: https://www.w3.org/TR/did-core/#fragment 44 | Fragment string 45 | } 46 | 47 | // URL represents the DID URI + A network location identifier for a specific resource 48 | // Spec: https://www.w3.org/TR/did-core/#did-url-syntax 49 | func (d DID) URL() string { 50 | url := d.URI 51 | if len(d.Params) > 0 { 52 | var pairs []string 53 | for key, value := range d.Params { 54 | pairs = append(pairs, fmt.Sprintf("%s=%s", key, value)) 55 | } 56 | url += ";" + strings.Join(pairs, ";") 57 | } 58 | if len(d.Path) > 0 { 59 | url += "/" + d.Path 60 | } 61 | if len(d.Query) > 0 { 62 | url += "?" + d.Query 63 | } 64 | if len(d.Fragment) > 0 { 65 | url += "#" + d.Fragment 66 | } 67 | return url 68 | } 69 | 70 | func (d DID) String() string { 71 | return d.URL() 72 | } 73 | 74 | // MarshalText will convert the given DID's URL into a byte array 75 | func (d DID) MarshalText() (text []byte, err error) { 76 | return []byte(d.String()), nil 77 | } 78 | 79 | // UnmarshalText will deserialize the given byte array into an instance of [DID] 80 | func (d *DID) UnmarshalText(text []byte) error { 81 | did, err := Parse(string(text)) 82 | if err != nil { 83 | return err 84 | } 85 | *d = did 86 | return nil 87 | } 88 | 89 | // Scan implements the Scanner interface 90 | func (d *DID) Scan(src any) error { 91 | switch obj := src.(type) { 92 | case nil: 93 | return nil 94 | case string: 95 | if src == "" { 96 | return nil 97 | } 98 | return d.UnmarshalText([]byte(obj)) 99 | default: 100 | return fmt.Errorf("unsupported scan type %T", obj) 101 | } 102 | } 103 | 104 | // Value implements the driver Valuer interface 105 | func (d DID) Value() (driver.Value, error) { 106 | return d.String(), nil 107 | } 108 | 109 | // relevant ABNF rules: https://www.w3.org/TR/did-core/#did-syntax 110 | var ( 111 | pctEncodedPattern = `(?:%[0-9a-fA-F]{2})` 112 | idCharPattern = `(?:[a-zA-Z0-9._-]|` + pctEncodedPattern + `)` 113 | methodPattern = `([a-z0-9]+)` 114 | methodIDPattern = `((?:` + idCharPattern + `*:)*(` + idCharPattern + `+))` 115 | paramCharPattern = `[a-zA-Z0-9_.:%-]` 116 | paramPattern = `;` + paramCharPattern + `+=` + paramCharPattern + `*` 117 | paramsPattern = `((` + paramPattern + `)*)` 118 | pathPattern = `(/[^#?]*)?` 119 | queryPattern = `(\?[^\#]*)?` 120 | fragmentPattern = `(\#.*)?` 121 | didURIPattern = regexp.MustCompile(`^did:` + methodPattern + `:` + methodIDPattern + paramsPattern + pathPattern + queryPattern + fragmentPattern + `$`) 122 | ) 123 | 124 | // Parse parses a DID URI in accordance to the ABNF rules specified in the 125 | // specification here: https://www.w3.org/TR/did-core/#did-syntax. Returns 126 | // a DIDURI instance if parsing is successful. Otherwise, returns an error. 127 | func Parse(input string) (DID, error) { 128 | match := didURIPattern.FindStringSubmatch(input) 129 | 130 | if match == nil { 131 | return DID{}, errors.New("invalid DID URI") 132 | } 133 | 134 | did := DID{ 135 | URI: "did:" + match[1] + ":" + match[2], 136 | Method: match[1], 137 | ID: match[2], 138 | } 139 | 140 | if len(match[4]) > 0 { 141 | params := strings.Split(match[4][1:], ";") 142 | parsedParams := make(map[string]string) 143 | for _, p := range params { 144 | kv := strings.Split(p, "=") 145 | parsedParams[kv[0]] = kv[1] 146 | } 147 | did.Params = parsedParams 148 | } 149 | 150 | if match[6] != "" { 151 | did.Path = match[6] 152 | } 153 | if match[7] != "" { 154 | did.Query = match[7][1:] 155 | } 156 | if match[8] != "" { 157 | did.Fragment = match[8][1:] 158 | } 159 | 160 | return did, nil 161 | } 162 | 163 | // MustParse parses a DID URI with Parse, and panics on error 164 | func MustParse(input string) DID { 165 | did, err := Parse(input) 166 | if err != nil { 167 | panic(err) 168 | } 169 | return did 170 | } 171 | -------------------------------------------------------------------------------- /dids/did/did_test.go: -------------------------------------------------------------------------------- 1 | package did_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/alecthomas/assert/v2" 7 | "github.com/decentralized-identity/web5-go/dids/did" 8 | ) 9 | 10 | type vector struct { 11 | input string 12 | output map[string]interface{} 13 | error bool 14 | } 15 | 16 | func TestParse(t *testing.T) { 17 | vectors := []vector{ 18 | {input: "", error: true}, 19 | {input: "did:", error: true}, 20 | {input: "did:uport", error: true}, 21 | {input: "did:uport:", error: true}, 22 | {input: "did:uport:1234_12313***", error: true}, 23 | {input: "2nQtiQG6Cgm1GYTBaaKAgr76uY7iSexUkqX", error: true}, 24 | {input: "did:method:%12%1", error: true}, 25 | {input: "did:method:%1233%Ay", error: true}, 26 | {input: "did:CAP:id", error: true}, 27 | {input: "did:method:id::anotherid%r9", error: true}, 28 | { 29 | input: "did:example:123456789abcdefghi", 30 | output: map[string]interface{}{ 31 | "method": "example", 32 | "id": "123456789abcdefghi", 33 | "uri": "did:example:123456789abcdefghi", 34 | }, 35 | }, 36 | { 37 | input: "did:example:123456789abcdefghi;foo=bar;baz=qux", 38 | output: map[string]interface{}{ 39 | "alternate": "did:example:123456789abcdefghi;baz=qux;foo=bar", 40 | "method": "example", 41 | "id": "123456789abcdefghi", 42 | "uri": "did:example:123456789abcdefghi", 43 | "params": map[string]string{ 44 | "foo": "bar", 45 | "baz": "qux", 46 | }, 47 | }, 48 | }, 49 | { 50 | input: "did:example:123456789abcdefghi?foo=bar&baz=qux", 51 | output: map[string]interface{}{ 52 | "method": "example", 53 | "id": "123456789abcdefghi", 54 | "uri": "did:example:123456789abcdefghi", 55 | "query": "foo=bar&baz=qux", 56 | }, 57 | }, 58 | { 59 | input: "did:example:123456789abcdefghi#keys-1", 60 | output: map[string]interface{}{ 61 | "method": "example", 62 | "id": "123456789abcdefghi", 63 | "uri": "did:example:123456789abcdefghi", 64 | "fragment": "keys-1", 65 | }, 66 | }, 67 | { 68 | input: "did:example:123456789abcdefghi?foo=bar&baz=qux#keys-1", 69 | output: map[string]interface{}{ 70 | "method": "example", 71 | "id": "123456789abcdefghi", 72 | "uri": "did:example:123456789abcdefghi", 73 | "query": "foo=bar&baz=qux", 74 | "fragment": "keys-1", 75 | }, 76 | }, 77 | { 78 | input: "did:example:123456789abcdefghi;foo=bar;baz=qux?p1=v1&p2=v2#keys-1", 79 | output: map[string]interface{}{ 80 | "alternate": "did:example:123456789abcdefghi;baz=quxfoo=bar;?p1=v1&p2=v2#keys-1", 81 | "method": "example", 82 | "id": "123456789abcdefghi", 83 | "uri": "did:example:123456789abcdefghi", 84 | "params": map[string]string{"foo": "bar", "baz": "qux"}, 85 | "query": "p1=v1&p2=v2", 86 | "fragment": "keys-1", 87 | }, 88 | }, 89 | } 90 | 91 | for _, v := range vectors { 92 | t.Run(v.input, func(t *testing.T) { 93 | did, err := did.Parse(v.input) 94 | 95 | if v.error && err == nil { 96 | t.Errorf("expected error, got nil") 97 | } 98 | 99 | if err != nil { 100 | if !v.error { 101 | t.Errorf("failed to parse did: %s", err.Error()) 102 | } 103 | return 104 | } 105 | 106 | // The Params map doesn't have a reliable order, so check both 107 | alt, ok := v.output["alternate"] 108 | if ok { 109 | firstOrder := v.input == did.URL() 110 | secondOrder := alt == did.URL() 111 | assert.True(t, firstOrder || secondOrder, "expected one of the orders to match") 112 | } else { 113 | assert.Equal[interface{}](t, v.input, did.URL()) 114 | } 115 | assert.Equal[interface{}](t, v.output["method"], did.Method) 116 | assert.Equal[interface{}](t, v.output["id"], did.ID) 117 | assert.Equal[interface{}](t, v.output["uri"], did.URI) 118 | 119 | if v.output["params"] != nil { 120 | params, ok := v.output["params"].(map[string]string) 121 | assert.True(t, ok, "expected params to be map[string]string") 122 | 123 | for k, v := range params { 124 | assert.Equal[interface{}](t, v, did.Params[k]) 125 | } 126 | } 127 | 128 | if v.output["query"] != nil { 129 | assert.Equal[interface{}](t, v.output["query"], did.Query) 130 | } 131 | 132 | if v.output["fragment"] != nil { 133 | assert.Equal[interface{}](t, v.output["fragment"], did.Fragment) 134 | } 135 | }) 136 | } 137 | } 138 | 139 | func TestDID_ScanValueRoundtrip(t *testing.T) { 140 | tests := []struct { 141 | object did.DID 142 | raw string 143 | alt string 144 | wantErr bool 145 | }{ 146 | { 147 | raw: "did:example:123456789abcdefghi", 148 | object: did.MustParse("did:example:123456789abcdefghi"), 149 | }, 150 | { 151 | raw: "did:example:123456789abcdefghi;foo=bar;baz=qux", 152 | alt: "did:example:123456789abcdefghi;baz=qux;foo=bar", 153 | object: did.MustParse("did:example:123456789abcdefghi;foo=bar;baz=qux"), 154 | }, 155 | { 156 | raw: "did:example:123456789abcdefghi?foo=bar&baz=qux", 157 | object: did.MustParse("did:example:123456789abcdefghi?foo=bar&baz=qux"), 158 | }, 159 | { 160 | raw: "did:example:123456789abcdefghi#keys-1", 161 | object: did.MustParse("did:example:123456789abcdefghi#keys-1"), 162 | }, 163 | { 164 | raw: "did:example:123456789abcdefghi?foo=bar&baz=qux#keys-1", 165 | object: did.MustParse("did:example:123456789abcdefghi?foo=bar&baz=qux#keys-1"), 166 | }, 167 | { 168 | raw: "did:example:123456789abcdefghi;foo=bar;baz=qux?foo=bar&baz=qux#keys-1", 169 | alt: "did:example:123456789abcdefghi;baz=qux;foo=bar?foo=bar&baz=qux#keys-1", 170 | object: did.MustParse("did:example:123456789abcdefghi;foo=bar;baz=qux?foo=bar&baz=qux#keys-1"), 171 | }, 172 | } 173 | for _, tt := range tests { 174 | t.Run(tt.raw, func(t *testing.T) { 175 | var d did.DID 176 | if err := d.Scan(tt.raw); (err != nil) != tt.wantErr { 177 | t.Errorf("Scan() error = %v, wantErr %v", err, tt.wantErr) 178 | } 179 | assert.Equal(t, tt.object, d) 180 | 181 | value, err := d.Value() 182 | assert.NoError(t, err) 183 | actual, ok := value.(string) 184 | assert.True(t, ok) 185 | if tt.alt != "" { 186 | assert.True(t, actual == tt.raw || actual == tt.alt) 187 | } else { 188 | assert.Equal(t, tt.raw, actual) 189 | } 190 | }) 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /dids/did/portabledid.go: -------------------------------------------------------------------------------- 1 | package did 2 | 3 | import ( 4 | "github.com/decentralized-identity/web5-go/dids/didcore" 5 | "github.com/decentralized-identity/web5-go/jwk" 6 | ) 7 | 8 | // PortableDID is a serializable BearerDID. VerificationMethod contains the private key 9 | // of each verification method that the BearerDID's key manager contains 10 | type PortableDID struct { 11 | // URI is the DID string as per https://www.w3.org/TR/did-core/#did-syntax 12 | URI string `json:"uri"` 13 | // PrivateKeys is an array of private keys associated to the BearerDID's verification methods 14 | // Note: PrivateKeys will be empty if the BearerDID was created using a KeyManager that does not 15 | // support exporting private keys (e.g. HSM based KeyManagers) 16 | PrivateKeys []jwk.JWK `json:"privateKeys"` 17 | // Document is the DID Document associated to the BearerDID 18 | Document didcore.Document `json:"document"` 19 | // Metadata is a map that can be used to store additional method specific data 20 | // that is necessary to inflate a BearerDID from a PortableDID 21 | Metadata map[string]interface{} `json:"metadata"` 22 | } 23 | -------------------------------------------------------------------------------- /dids/didcore/document_test.go: -------------------------------------------------------------------------------- 1 | package didcore_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/alecthomas/assert/v2" 7 | "github.com/decentralized-identity/web5-go/dids/didcore" 8 | ) 9 | 10 | func TestAddVerificationMethod(t *testing.T) { 11 | doc := didcore.Document{ 12 | Context: []string{"https://www.w3.org/ns/did/v1"}, 13 | ID: "did:example:123456789abcdefghi", 14 | } 15 | 16 | vm := didcore.VerificationMethod{ 17 | ID: "did:example:123456789abcdefghi#keys-1", 18 | Type: "Ed25519VerificationKey2018", 19 | Controller: "did:example:123456789abcdefghi", 20 | } 21 | 22 | doc.AddVerificationMethod(vm, didcore.Purposes("authentication")) 23 | 24 | assert.Equal(t, 1, len(doc.VerificationMethod)) 25 | assert.Equal(t, 1, len(doc.Authentication)) 26 | assert.Equal(t, vm.ID, doc.Authentication[0]) 27 | } 28 | 29 | func TestWoo(t *testing.T) { 30 | doc := didcore.Document{ 31 | ID: "did:example:123456789abcdefghi", 32 | } 33 | 34 | doc.AddVerificationMethod(didcore.VerificationMethod{ 35 | ID: "did:example:123456789abcdefghi#keys-1", 36 | Type: "Ed25519VerificationKey2018", 37 | Controller: "did:example:123456789abcdefghi", 38 | }, didcore.Purposes("authentication")) 39 | 40 | vm, err := doc.SelectVerificationMethod(didcore.Purpose("authentication")) 41 | assert.NoError(t, err) 42 | assert.Equal(t, "did:example:123456789abcdefghi#keys-1", vm.ID) 43 | } 44 | -------------------------------------------------------------------------------- /dids/didcore/resolution.go: -------------------------------------------------------------------------------- 1 | package didcore 2 | 3 | import "context" 4 | 5 | // MethodResolver is an interface that can be implemented for resolving specific DID methods. 6 | // Each concrete implementation should adhere to the DID core specficiation defined here: 7 | // https://www.w3.org/TR/did-core/#did-resolution 8 | type MethodResolver interface { 9 | Resolve(uri string) (ResolutionResult, error) 10 | ResolveWithContext(ctx context.Context, uri string) (ResolutionResult, error) 11 | } 12 | 13 | // ResolutionResult represents the result of a DID (Decentralized Identifier) 14 | // resolution. 15 | // 16 | // This class encapsulates the metadata and document information obtained as 17 | // a result of resolving a DID. It includes the resolution metadata, the DID 18 | // document (if available), and the document metadata. 19 | // 20 | // The `DidResolutionResult` can be initialized with specific metadata and 21 | // document information, or it can be created with default values if no 22 | // specific information is provided. 23 | type ResolutionResult struct { 24 | // The metadata associated with the DID resolution process. 25 | // 26 | // This includes information about the resolution process itself, such as any errors 27 | // that occurred. If not provided in the constructor, it defaults to an empty object 28 | // as per the spec 29 | ResolutionMetadata ResolutionMetadata `json:"didResolutionMetadata,omitempty"` 30 | // The resolved DID document, if available. 31 | // 32 | // This is the document that represents the resolved state of the DID. It may be `null` 33 | // if the DID could not be resolved or if the document is not available. 34 | Document Document `json:"didDocument"` 35 | // The metadata associated with the DID document. 36 | // 37 | // This includes information about the document such as when it was created and 38 | // any other relevant metadata. If not provided in the constructor, it defaults to an 39 | // empty `DidDocumentMetadata`. 40 | DocumentMetadata DocumentMetadata `json:"didDocumentMetadata,omitempty"` 41 | } 42 | 43 | // ResolutionResultWithError creates a Resolution Result populated with all default values and the error code provided. 44 | func ResolutionResultWithError(errorCode string) ResolutionResult { 45 | return ResolutionResult{ 46 | ResolutionMetadata: ResolutionMetadata{ 47 | Error: errorCode, 48 | }, 49 | DocumentMetadata: DocumentMetadata{}, 50 | } 51 | } 52 | 53 | // ResolutionResultWithDocument creates a Resolution Result populated with all default values and the document provided. 54 | func ResolutionResultWithDocument(document Document) ResolutionResult { 55 | return ResolutionResult{ 56 | ResolutionMetadata: ResolutionMetadata{}, 57 | Document: document, 58 | DocumentMetadata: DocumentMetadata{}, 59 | } 60 | } 61 | 62 | // GetError returns the error code associated with the resolution result. returns an empty string if no error code is present. 63 | func (r *ResolutionResult) GetError() string { 64 | return r.ResolutionMetadata.Error 65 | } 66 | 67 | // ResolutionMetadata is a metadata structure consisting of values relating to the results of the 68 | // DID resolution process which typically changes between invocations of the 69 | // resolve and resolveRepresentation functions, as it represents data about 70 | // the resolution process itself 71 | // 72 | // Spec: https://www.w3.org/TR/did-core/#dfn-didresolutionmetadata 73 | type ResolutionMetadata struct { 74 | // The Media Type of the returned didDocumentStream. This property is 75 | // REQUIRED if resolution is successful and if the resolveRepresentation 76 | // function was called 77 | ContentType string `json:"contentType,omitempty"` 78 | 79 | // The error code from the resolution process. This property is REQUIRED 80 | // when there is an error in the resolution process. The value of this 81 | // property MUST be a single keyword ASCII string. The possible property 82 | // values of this field SHOULD be registered in the 83 | // [DID Specification Registries](https://www.w3.org/TR/did-spec-registries/#error) 84 | Error string `json:"error,omitempty"` 85 | } 86 | 87 | // ResolutionError represents the error field of a ResolutionMetadata object. This struct implements error and is used to 88 | // surface the error code from the resolution process. it is returned as the error value from resolve as a means to 89 | // support idiomatic go error handling while also remaining spec compliant. It's worth mentioning that the spec expects 90 | // error to be returned within ResolutionMedata. Given this, the error code is also present on ResolutionMetadata whenever 91 | // an error occurs 92 | // well known code values can be found here: https://www.w3.org/TR/did-spec-registries/#error 93 | type ResolutionError struct { 94 | Code string 95 | } 96 | 97 | func (e ResolutionError) Error() string { 98 | return e.Code 99 | } 100 | -------------------------------------------------------------------------------- /dids/diddht/internal/bencode/bencode.go: -------------------------------------------------------------------------------- 1 | package bencode 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "strconv" 8 | ) 9 | 10 | const ( 11 | DictionaryPrefix = 'd' 12 | IntegerPrefix = 'i' 13 | ListPrefix = 'l' 14 | EndSuffix = 'e' 15 | ) 16 | 17 | // Marshal encodes the given input into a Bencode formatted byte array. 18 | // Note: Does not support encoding structs at the moment. 19 | // More information about Bencode can be found at: 20 | // https://wiki.theory.org/BitTorrentSpecification#Bencoding 21 | func Marshal(input any) ([]byte, error) { 22 | switch v := input.(type) { 23 | case string: 24 | encoded := fmt.Sprintf("%d:%s", len(v), v) 25 | 26 | return []byte(encoded), nil 27 | case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64: 28 | encoded := fmt.Sprintf("%c%d%c", IntegerPrefix, v, EndSuffix) 29 | 30 | return []byte(encoded), nil 31 | case []byte: 32 | size := fmt.Sprintf("%d:", len(v)) 33 | encoded := append([]byte(size), v...) 34 | 35 | return encoded, nil 36 | case []any: 37 | var b []byte 38 | b = append(b, ListPrefix) 39 | 40 | for _, item := range v { 41 | encoded, err := Marshal(item) 42 | if err != nil { 43 | return nil, err 44 | } 45 | 46 | b = append(b, encoded...) 47 | } 48 | 49 | b = append(b, 'e') 50 | 51 | return b, nil 52 | case map[string]any: 53 | var b []byte 54 | b = append(b, 'd') 55 | 56 | for key, value := range v { 57 | encodedKey, err := Marshal(key) 58 | if err != nil { 59 | return nil, err 60 | } 61 | 62 | encodedValue, err := Marshal(value) 63 | if err != nil { 64 | return nil, err 65 | } 66 | 67 | b = append(b, encodedKey...) 68 | b = append(b, encodedValue...) 69 | } 70 | 71 | b = append(b, 'e') 72 | 73 | return b, nil 74 | default: 75 | return nil, fmt.Errorf("unsupported type: %T", input) 76 | } 77 | } 78 | 79 | // Unmarshal decodes a Bencode formatted byte array into the the type of the provided output. 80 | // More information can be found at: 81 | // https://wiki.theory.org/BitTorrentSpecification#Bencoding 82 | func Unmarshal(input []byte, output any) error { 83 | switch v := output.(type) { 84 | case *string: 85 | _, err := unmarshalString(input, v) 86 | if err != nil { 87 | return fmt.Errorf("failed to unmarshal string: %w", err) 88 | } 89 | case *int: 90 | _, err := unmarshalInt(input, v) 91 | if err != nil { 92 | return fmt.Errorf("failed to unmarshal int: %w", err) 93 | } 94 | case *[]any: 95 | _, err := unmarshalList(input, v) 96 | if err != nil { 97 | return fmt.Errorf("failed to unmarshal list: %w", err) 98 | } 99 | case *map[string]any: 100 | _, err := unmarshalDict(input, *v) 101 | if err != nil { 102 | return fmt.Errorf("failed to unmarshal dict: %w", err) 103 | } 104 | default: 105 | return fmt.Errorf("unsupported type: %T", output) 106 | } 107 | 108 | return nil 109 | } 110 | 111 | // unmarshalValue decodes a Bencode value from a byte slice and returns 112 | // the decoded value, the # of bytes processed, and an error if any. 113 | func unmarshalValue(input []byte) (any, int, error) { 114 | switch input[0] { 115 | case IntegerPrefix: 116 | var value int 117 | n, err := unmarshalInt(input, &value) 118 | if err != nil { 119 | return nil, 0, err 120 | } 121 | 122 | return value, n, nil 123 | case ListPrefix: 124 | value := make([]any, 0) 125 | n, err := unmarshalList(input, &value) 126 | if err != nil { 127 | return nil, 0, err 128 | } 129 | 130 | return value, n, nil 131 | case DictionaryPrefix: 132 | value := make(map[string]any) 133 | n, err := unmarshalDict(input, value) 134 | if err != nil { 135 | return nil, 0, err 136 | } 137 | 138 | return value, n, nil 139 | default: 140 | var value string 141 | n, err := unmarshalString(input, &value) 142 | if err != nil { 143 | return nil, 0, err 144 | } 145 | 146 | return value, n, nil 147 | } 148 | } 149 | 150 | // unmarshalString decodes a Bencode string from a byte slice. 151 | // It returns the total bytes processed, and an error if any. 152 | func unmarshalString(input []byte, output *string) (int, error) { 153 | // Find the colon index, which separates the length part from the data part. 154 | colonIndex := bytes.IndexByte(input, ':') 155 | if colonIndex == -1 { 156 | return 0, errors.New("colon not found in input") 157 | } 158 | 159 | // Extract the length part and convert it to an integer. 160 | lengthPart := input[:colonIndex] 161 | length, err := strconv.Atoi(string(lengthPart)) 162 | if err != nil { 163 | return 0, fmt.Errorf("failed to convert length: %w", err) 164 | } 165 | 166 | // Calculate the start and end of the actual string data. 167 | start := colonIndex + 1 168 | end := start + length 169 | 170 | // Check if the calculated end exceeds the input length. 171 | if end > len(input) { 172 | return 0, errors.New("data length exceeds input length") 173 | } 174 | 175 | // Extract and return the actual string data. 176 | *output = string(input[start:end]) 177 | return end, nil // end is the total bytes processed. 178 | } 179 | 180 | // unmarshalInt decodes a Bencode integer from a byte slice. 181 | // It returns the total bytes processed, and an error if any. 182 | func unmarshalInt(input []byte, output *int) (int, error) { 183 | if input[0] != IntegerPrefix { 184 | return 0, fmt.Errorf("input does not start with %q", IntegerPrefix) 185 | } 186 | 187 | // Find the suffix byte which marks the end of the integer. 188 | endIndex := bytes.IndexByte(input, EndSuffix) 189 | if endIndex == -1 { 190 | return 0, errors.New("end byte not found in input") 191 | } 192 | 193 | // Extract the data between prefix and suffix bytes and convert it to an integer. 194 | intPart := input[1:endIndex] 195 | str := string(intPart) 196 | value, err := strconv.Atoi(str) 197 | if err != nil { 198 | return 0, fmt.Errorf("failed to convert %s into int: %w", str, err) 199 | } 200 | 201 | // Assign the decoded integer to the output. 202 | *output = value 203 | return endIndex + 1, nil // endIndex + 1 is the total bytes processed. 204 | } 205 | 206 | // unmarshalList decodes a Bencode list from a byte slice. 207 | // It returns the total bytes processed, and an error if any. 208 | func unmarshalList(input []byte, output *[]any) (int, error) { 209 | // Iterate over the input bytes and decode each list item. 210 | i := 1 // Skip the prefix byte. 211 | for i < len(input) { 212 | // Check if we have reached the end of the list. 213 | if input[i] == EndSuffix { 214 | return i + 1, nil // i + 1 is the total bytes processed. 215 | } 216 | 217 | value, n, err := unmarshalValue(input[i:]) 218 | if err != nil { 219 | return 0, fmt.Errorf("failed to decode list item: %w", err) 220 | } 221 | 222 | *output = append(*output, value) 223 | i += n 224 | } 225 | 226 | return i, errors.New("unexpected end of input") 227 | } 228 | 229 | // unmarshalDict decodes a Bencode dictionary from a byte slice. 230 | // It returns the total bytes processed, and an error if any. 231 | func unmarshalDict(input []byte, output map[string]any) (int, error) { 232 | if input[0] != DictionaryPrefix { 233 | return 0, errors.New("input does not start with 'd'") 234 | } 235 | 236 | // Iterate over the input bytes and decode each key-value pair. 237 | i := 1 // Skip the prefix byte. 238 | for i < len(input) { 239 | // Check if we have reached the end of the dictionary. 240 | if input[i] == EndSuffix { 241 | return i + 1, nil // i + 1 is the total bytes processed. 242 | } 243 | 244 | // Decode the key. 245 | var key string 246 | n, err := unmarshalString(input[i:], &key) 247 | if err != nil { 248 | return 0, fmt.Errorf("failed to decode key: %w", err) 249 | } 250 | i += n 251 | 252 | value, n, err := unmarshalValue(input[i:]) 253 | if err != nil { 254 | return 0, fmt.Errorf("failed to decode value: %w", err) 255 | } 256 | 257 | output[key] = value 258 | i += n 259 | } 260 | 261 | return i, errors.New("unexpected end of input") 262 | } 263 | -------------------------------------------------------------------------------- /dids/diddht/internal/bencode/bencode_test.go: -------------------------------------------------------------------------------- 1 | package bencode_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/alecthomas/assert/v2" 7 | "github.com/decentralized-identity/web5-go/dids/diddht/internal/bencode" 8 | ) 9 | 10 | func TestMarshal_String(t *testing.T) { 11 | type vector struct { 12 | input string 13 | expected []byte 14 | } 15 | 16 | vectors := []vector{ 17 | {"spam", []byte("4:spam")}, 18 | {"", []byte("0:")}, 19 | } 20 | 21 | for _, v := range vectors { 22 | actual, err := bencode.Marshal(v.input) 23 | assert.NoError(t, err) 24 | 25 | assert.Equal(t, v.expected, actual) 26 | } 27 | } 28 | 29 | func TestMarshal_Int(t *testing.T) { 30 | type vector struct { 31 | input int 32 | expected []byte 33 | } 34 | 35 | vectors := []vector{ 36 | {42, []byte("i42e")}, 37 | {0, []byte("i0e")}, 38 | } 39 | 40 | for _, v := range vectors { 41 | actual, err := bencode.Marshal(v.input) 42 | assert.NoError(t, err) 43 | 44 | assert.Equal(t, v.expected, actual) 45 | } 46 | } 47 | 48 | func TestMarshal_List(t *testing.T) { 49 | input := []any{"spam", "eggs"} 50 | expected := []byte("l4:spam4:eggse") 51 | 52 | actual, err := bencode.Marshal(input) 53 | assert.NoError(t, err) 54 | 55 | assert.Equal(t, expected, actual) 56 | } 57 | 58 | func TestMarshal_Dict(t *testing.T) { 59 | input := map[string]any{ 60 | "spam": []any{"a", "b"}, 61 | } 62 | 63 | expected := []byte("d4:spaml1:a1:bee") 64 | 65 | actual, err := bencode.Marshal(input) 66 | assert.NoError(t, err) 67 | assert.Equal(t, expected, actual) 68 | } 69 | 70 | func TestUnmarshal_String(t *testing.T) { 71 | input := []byte("4:spam") 72 | var output string 73 | 74 | err := bencode.Unmarshal(input, &output) 75 | assert.NoError(t, err) 76 | assert.Equal(t, "spam", output) 77 | } 78 | 79 | func TestUnmarshal_Int(t *testing.T) { 80 | input := []byte("i42e") 81 | var output int 82 | 83 | err := bencode.Unmarshal(input, &output) 84 | assert.NoError(t, err) 85 | assert.Equal(t, 42, output) 86 | } 87 | 88 | func TestUnmarshal_List(t *testing.T) { 89 | type vector struct { 90 | input []byte 91 | expected []any 92 | } 93 | 94 | vectors := []vector{ 95 | { 96 | input: []byte("l4:spam4:eggse"), 97 | expected: []any{"spam", "eggs"}, 98 | }, 99 | { 100 | input: []byte("le"), 101 | expected: []any{}, 102 | }, 103 | } 104 | 105 | for _, v := range vectors { 106 | output := make([]any, 0) 107 | err := bencode.Unmarshal(v.input, &output) 108 | 109 | assert.NoError(t, err) 110 | assert.Equal(t, len(v.expected), len(output)) 111 | 112 | for i, expected := range v.expected { 113 | assert.Equal(t, expected, output[i]) 114 | } 115 | } 116 | 117 | } 118 | 119 | func TestUnmarshal_Dict(t *testing.T) { 120 | 121 | type vector struct { 122 | input []byte 123 | expected map[string]any 124 | } 125 | 126 | vectors := []vector{ 127 | { 128 | input: []byte("d9:publisher3:bob17:publisher-webpage15:www.example.com18:publisher.location4:homee"), 129 | expected: map[string]any{ 130 | "publisher": "bob", 131 | "publisher-webpage": "www.example.com", 132 | "publisher.location": "home", 133 | }, 134 | }, 135 | { 136 | input: []byte("d3:cow3:moo4:spam4:eggse"), 137 | expected: map[string]any{ 138 | "cow": "moo", 139 | "spam": "eggs", 140 | }, 141 | }, 142 | { 143 | input: []byte("de"), 144 | expected: map[string]any{}, 145 | }, 146 | } 147 | 148 | for _, v := range vectors { 149 | output := make(map[string]any) 150 | err := bencode.Unmarshal(v.input, &output) 151 | assert.NoError(t, err) 152 | assert.Equal(t, len(v.expected), len(output)) 153 | 154 | for k, expected := range v.expected { 155 | assert.Equal(t, expected, output[k]) 156 | } 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /dids/diddht/internal/bep44/bep44.go: -------------------------------------------------------------------------------- 1 | package bep44 2 | 3 | import ( 4 | "encoding/binary" 5 | "errors" 6 | "fmt" 7 | ) 8 | 9 | // Message Represents a BEP44 message, which is used for storing and retrieving data in the Mainline DHT 10 | // network. 11 | // 12 | // A BEP44 message is used primarily in the context of the DID DHT method for publishing and 13 | // resolving DID documents in the DHT network. This type encapsulates the data structure required 14 | // for such operations in accordance with BEP44. 15 | // 16 | // https://www.bittorrent.org/beps/bep_0044.html 17 | type Message struct { 18 | 19 | // The public key bytes of the Identity Key, which serves as the identifier in the DHT network for 20 | // the corresponding BEP44 message. 21 | // 22 | k []byte 23 | 24 | // The sequence number of the message, used to ensure the latest version of the data is retrieved 25 | // and updated. It's a monotonically increasing number. 26 | Seq int64 27 | 28 | // The signature of the message, ensuring the authenticity and integrity of the data. It's 29 | // computed over the bencoded sequence number and value. 30 | sig []byte 31 | 32 | // The actual data being stored or retrieved from the DHT network, typically encoded in a format 33 | // suitable for DNS packet representation of a DID Document. 34 | V []byte 35 | } 36 | 37 | // Signer is a function that signs a given payload and returns the signature. 38 | type Signer func(payload []byte) ([]byte, error) 39 | 40 | // NewMessage bencodes the payload, signes it with the signer and creates a new BEP44 message with the given sequence number, public key. 41 | func NewMessage(dnsPayload []byte, seq int64, publicKeyBytes []byte, signer Signer) (*Message, error) { 42 | bencodedBytes, err := bencodeBepPayload(seq, dnsPayload) 43 | if err != nil { 44 | return nil, fmt.Errorf("failed to bencode payload: %w", err) 45 | } 46 | 47 | // remove the 1st (d) and last (e) byte from the bencoded bytes to conform to the BEP44 spec 48 | // and sign the payload 49 | signedBytes, err := signer(bencodedBytes) 50 | if err != nil { 51 | return nil, fmt.Errorf("failed to sign: %w", err) 52 | } 53 | 54 | bep := &Message{ 55 | k: publicKeyBytes, 56 | Seq: seq, 57 | sig: signedBytes, 58 | V: dnsPayload, 59 | } 60 | 61 | return bep, nil 62 | } 63 | 64 | // Marshal encodes the BEP44 message into a byte slice, conforming to the Pkarr relay specification. 65 | func (msg *Message) Marshal() ([]byte, error) { 66 | // Construct the body of the request according to the Pkarr relay specification. 67 | body := make([]byte, 0, len(msg.V)+72) 68 | body = append(body, msg.sig...) 69 | 70 | // Convert the sequence number to a big-endian byte array. 71 | seq := uint64(msg.Seq) 72 | buf := make([]byte, 8) // uint64 is 8 bytes 73 | binary.BigEndian.PutUint64(buf, seq) 74 | body = append(body, buf...) 75 | body = append(body, msg.V...) 76 | 77 | return body, nil 78 | } 79 | 80 | // UnmarshalMessage decodes the given byte slice into a BEP44 message. 81 | func UnmarshalMessage(data []byte, b *Message) error { 82 | if len(data) < 72 { 83 | return fmt.Errorf("pkarr response must be at least 72 bytes but got: %d", len(data)) 84 | } 85 | 86 | if len(data) > 1072 { 87 | return fmt.Errorf("pkarr response is larger than 1072 bytes, got: %d", len(data)) 88 | } 89 | 90 | b.sig = data[:64] 91 | b.Seq = int64(binary.BigEndian.Uint64(data[64:72])) 92 | b.V = data[72:] 93 | 94 | return nil 95 | } 96 | 97 | func bencodeBepPayload(seq int64, v []byte) ([]byte, error) { 98 | if len(v) == 0 { 99 | return nil, errors.New("v cannot be empty") 100 | } 101 | 102 | re := fmt.Sprintf("3:seqi%de1:v%d:%s", seq, len(v), v) 103 | if len(re) > 1000 { 104 | return nil, errors.New("bencoded payload is too large") 105 | } 106 | return []byte(re), nil 107 | } 108 | -------------------------------------------------------------------------------- /dids/diddht/internal/bep44/bep44_test.go: -------------------------------------------------------------------------------- 1 | package bep44 2 | 3 | import ( 4 | "crypto/ed25519" 5 | "errors" 6 | "fmt" 7 | "testing" 8 | "time" 9 | 10 | "github.com/alecthomas/assert/v2" 11 | ) 12 | 13 | func Test_newSignedBEP44Message(t *testing.T) { 14 | payload := []byte(`v=1,b=2,c=3`) 15 | 16 | pubKey, privKey, err := ed25519.GenerateKey(nil) 17 | assert.NoError(t, err) 18 | type args struct { 19 | payload []byte 20 | seq int64 21 | publicKeyBytes []byte 22 | signer Signer 23 | } 24 | tests := map[string]struct { 25 | args args 26 | wantErr bool 27 | }{ 28 | "good - create signed message and decode payload": { 29 | args: args{ 30 | payload: payload, 31 | seq: time.Date(2021, time.January, 1, 0, 0, 0, 0, time.UTC).Unix() / 1000, 32 | publicKeyBytes: pubKey, 33 | signer: func(payload []byte) ([]byte, error) { 34 | return ed25519.Sign(privKey, payload), nil 35 | }, 36 | }, 37 | }, 38 | "bad - signer fails": { 39 | args: args{ 40 | payload: payload, 41 | seq: time.Date(2021, time.January, 1, 0, 0, 0, 0, time.UTC).Unix() / 1000, 42 | publicKeyBytes: []byte("YCcHYL2sYNPDlKaALcEmll2HHyT968M4UWbr-9CFGWE"), 43 | signer: func(payload []byte) ([]byte, error) { 44 | return nil, errors.New("signer failed") 45 | }, 46 | }, 47 | wantErr: true, 48 | }, 49 | } 50 | 51 | for testName, tt := range tests { 52 | t.Run(testName, func(t *testing.T) { 53 | got, err := NewMessage(tt.args.payload, tt.args.seq, tt.args.publicKeyBytes, tt.args.signer) 54 | assert.Equal(t, tt.wantErr, err != nil) 55 | if tt.wantErr { 56 | return 57 | } 58 | assert.Equal(t, tt.args.publicKeyBytes, got.k) 59 | 60 | bencodedBytes, err := bencodeBepPayload(tt.args.seq, tt.args.payload) 61 | assert.NoError(t, err) 62 | verified := ed25519.Verify(tt.args.publicKeyBytes, bencodedBytes, got.sig) 63 | if !verified { 64 | fmt.Println(string(bencodedBytes), got.sig, tt) 65 | } 66 | assert.True(t, verified) 67 | }) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /dids/diddht/internal/dns/constants.go: -------------------------------------------------------------------------------- 1 | package dns 2 | 3 | const ( 4 | // Labels for the dns representation of the verification method purposes 5 | 6 | // PurposeAuthentication is the DNS representation of the authentication purpose 7 | PurposeAuthentication = "auth" 8 | 9 | // PurposeAssertionMethod is the DNS representation of the assertion method purpose 10 | PurposeAssertionMethod = "asm" 11 | 12 | // PurposeCapabilityDeletion is the DNS representation of the capability delegation purpose 13 | PurposeCapabilityDeletion = "del" 14 | 15 | // PurposeCapabilityInvocation is the DNS representation of the capability invocation purpose 16 | PurposeCapabilityInvocation = "inv" 17 | 18 | // PurposeKeyAgreement is the DNS representation of the key agreement purpose 19 | PurposeKeyAgreement = "agm" 20 | 21 | // Labels for other properties 22 | 23 | // DNSLabelVerificationMethod is the DNS representation of the verification method property 24 | DNSLabelVerificationMethod = "vm" 25 | 26 | // DNSLabelService is the DNS representation of the service property 27 | DNSLabelService = "srv" 28 | 29 | // DNSLabelController is the DNS representation of the controller property 30 | DNSLabelController = "cnt" 31 | 32 | // DNSLabelAlsoKnownAs is the DNS representation of the AKA property 33 | DNSLabelAlsoKnownAs = "aka" 34 | ) 35 | -------------------------------------------------------------------------------- /dids/diddht/internal/dns/did.go: -------------------------------------------------------------------------------- 1 | package dns 2 | 3 | import ( 4 | "encoding/base64" 5 | "errors" 6 | "fmt" 7 | "net/url" 8 | "sort" 9 | "strings" 10 | 11 | "github.com/decentralized-identity/web5-go/crypto/dsa" 12 | "github.com/decentralized-identity/web5-go/dids/didcore" 13 | "golang.org/x/net/dns/dnsmessage" 14 | ) 15 | 16 | // MarshalDIDDocument packs a DID document into a TXT DNS resource records and adds to the DNS message Answers 17 | func MarshalDIDDocument(d *didcore.Document) ([]byte, error) { 18 | 19 | // create root record 20 | var msg dnsmessage.Message 21 | var vmIDToK = make(map[string]string) 22 | var vmBEP44Keys []string 23 | // get sorted VM IDs 24 | sortedIDs := pluckSort(d.VerificationMethod) 25 | 26 | for k, id := range sortedIDs { 27 | _k := fmt.Sprintf("k%d", k) 28 | vmIDToK[id] = _k 29 | vmBEP44Keys = append(vmBEP44Keys, _k) 30 | } 31 | 32 | var sToK = make(map[string]string) 33 | var sKeys []string 34 | for k, v := range d.Service { 35 | _k := fmt.Sprintf("s%d", k) 36 | sToK[v.ID] = _k 37 | sKeys = append(sKeys, _k) 38 | } 39 | 40 | rootProps := map[string][]string{ 41 | "v": {"1"}, 42 | "id": {d.ID}, 43 | DNSLabelVerificationMethod: vmBEP44Keys, 44 | PurposeAuthentication: methodsToKeys(d.Authentication, vmIDToK), 45 | PurposeAssertionMethod: methodsToKeys(d.AssertionMethod, vmIDToK), 46 | PurposeKeyAgreement: methodsToKeys(d.KeyAgreement, vmIDToK), 47 | PurposeCapabilityInvocation: methodsToKeys(d.CapabilityInvocation, vmIDToK), 48 | PurposeCapabilityDeletion: methodsToKeys(d.CapabilityDelegation, vmIDToK), 49 | DNSLabelService: sKeys, 50 | DNSLabelController: d.Controller, 51 | DNSLabelAlsoKnownAs: d.AlsoKnownAs, 52 | } 53 | 54 | var rootPropsSerialized []string 55 | for k, v := range rootProps { 56 | if len(v) == 0 { 57 | continue 58 | } 59 | prop := fmt.Sprintf("%s=%s", k, strings.Join(v, ",")) 60 | rootPropsSerialized = append(rootPropsSerialized, prop) 61 | } 62 | 63 | id := strings.TrimPrefix(d.ID, "did:dht:") 64 | resource, err := newResource(fmt.Sprintf("_did.%s.", id), strings.Join(rootPropsSerialized, ";")) 65 | if err != nil { 66 | return nil, err 67 | } 68 | msg.Answers = append(msg.Answers, resource) 69 | 70 | // add verification methods to dns message 71 | for _, vm := range d.VerificationMethod { 72 | // look for the key after the # in the verification method ID 73 | key, ok := vmIDToK[vm.ID] 74 | if !ok { 75 | // TODO handle error 76 | continue 77 | } 78 | buf, err := MarshalVerificationMethod(&vm) 79 | if err != nil { 80 | return nil, err 81 | } 82 | 83 | resource, err := newResource(fmt.Sprintf("_%s._did.", key), buf) 84 | 85 | if err != nil { 86 | return nil, err 87 | } 88 | 89 | msg.Answers = append(msg.Answers, resource) 90 | } 91 | 92 | // add services to dns message 93 | for _, s := range d.Service { 94 | key, ok := sToK[s.ID] 95 | if !ok { 96 | // TODO handle error 97 | continue 98 | } 99 | if err := MarshalService(key, s, &msg); err != nil { 100 | return nil, err 101 | } 102 | } 103 | 104 | msgByes, err := msg.Pack() 105 | if err != nil { 106 | return nil, err 107 | } 108 | 109 | return msgByes, nil 110 | } 111 | 112 | // UnmarshalDIDDocument unpacks the TXT DNS resource records and returns a DID document 113 | func UnmarshalDIDDocument(payload []byte) (*didcore.Document, error) { 114 | decoder, err := parseDNSDID(payload) 115 | if err != nil { 116 | return nil, err 117 | } 118 | 119 | doc, err := decoder.DIDDocument() 120 | if err != nil { 121 | return nil, err 122 | } 123 | 124 | return doc, nil 125 | } 126 | 127 | // MarshalVerificationMethod packs a verification method into a TXT DNS resource record and adds to the DNS message Answers 128 | func MarshalVerificationMethod(vm *didcore.VerificationMethod) (string, error) { 129 | keyBytes, err := dsa.PublicKeyToBytes(*vm.PublicKeyJwk) 130 | if err != nil { 131 | return "", err 132 | } 133 | 134 | algID, err := dsa.AlgorithmID(vm.PublicKeyJwk) 135 | if err != nil { 136 | return "", err 137 | } 138 | t, ok := algToDhtIndex[algID] 139 | if !ok { 140 | return "", errors.New("unsupported algorithm") 141 | } 142 | 143 | // TODO: clean this up. smol monkey patch that accommodates 144 | // the possibility that vm.ID may or may not have a '#'. 145 | // Technically it always should but i can't confirm that given that 146 | // this fix is blocking other work. did:dht will be refactored in 147 | // a subsequent PR soon (Moe: 2024-04-16) 148 | splitID := strings.Split(vm.ID, "#") 149 | fragment := splitID[len(splitID)-1] 150 | props := []string{ 151 | "id=" + fragment, 152 | "t=" + t, 153 | "k=" + base64.RawURLEncoding.EncodeToString(keyBytes), 154 | } 155 | 156 | // TODO: controller should only be set in the TXT record if the value is different from document.id (Moe: 2024-04-16) 157 | if len(vm.Controller) > 0 { 158 | props = append(props, "c="+vm.Controller) 159 | } 160 | 161 | dhtEncodedVM := strings.Join(props, ";") 162 | 163 | return dhtEncodedVM, nil 164 | 165 | } 166 | 167 | // MarshalService packs a service into a TXT DNS resource record and adds to the DNS message Answers 168 | func MarshalService(dhtDNSkey string, s didcore.Service, msg *dnsmessage.Message) error { 169 | rawData := fmt.Sprintf("id=%s;t=%s;se=%s", s.ID, s.Type, strings.Join(s.ServiceEndpoint, ",")) 170 | 171 | resource, err := newResource(fmt.Sprintf("_%s._did.", dhtDNSkey), rawData) 172 | if err != nil { 173 | return err 174 | } 175 | msg.Answers = append(msg.Answers, resource) 176 | return nil 177 | } 178 | 179 | // UnmarshalVerificationMethod unpacks the TXT DNS resource encoded verification method 180 | func UnmarshalVerificationMethod(data string, did string, vm *didcore.VerificationMethod) error { 181 | propertyMap, err := parseTXTRecordData(data) 182 | if err != nil { 183 | return err 184 | } 185 | 186 | vm.Type = "JsonWebKey" 187 | 188 | var key string 189 | var algorithmID string 190 | for property, v := range propertyMap { 191 | switch property { 192 | // According to https://did-dht.com/#verification-methods, this should not be a list 193 | case "id": 194 | vm.ID = did + "#" + strings.Join(v, "") 195 | case "t": // Index of the key type https://did-dht.com/registry/index.html#key-type-index 196 | algorithmID, _ = dhtIndexToAlg[strings.Join(v, "")] 197 | case "k": // unpadded base64URL representation of the public key 198 | key = strings.Join(v, "") 199 | case "c": // the controller is optional 200 | vm.Controller = strings.Join(v, "") 201 | default: 202 | continue 203 | } 204 | } 205 | 206 | // if controller is omitted from the record, it is assumed that controller is document.ID 207 | if vm.Controller == "" { 208 | vm.Controller = did 209 | } 210 | 211 | if len(key) == 0 || len(algorithmID) == 0 { 212 | return errors.New("unable to parse public key") 213 | } 214 | 215 | // RawURLEncoding is the same as URLEncoding but omits padding. 216 | // Decoding and reencoding to make sure there is no padding 217 | keyBytes, err := base64.RawURLEncoding.DecodeString(key) 218 | if err != nil { 219 | return err 220 | } 221 | 222 | if len(keyBytes) == 0 { 223 | return errors.New("malformed public key") 224 | } 225 | 226 | j, err := dsa.BytesToPublicKey(algorithmID, keyBytes) 227 | if err != nil { 228 | return err 229 | } 230 | vm.PublicKeyJwk = &j 231 | 232 | // validate all the parts exist 233 | if len(vm.ID) == 0 || vm.PublicKeyJwk == nil { 234 | return errors.New("malformed verification method representation") 235 | } 236 | 237 | return nil 238 | } 239 | 240 | // UnmarshalService unpacks the TXT DNS resource encoded service 241 | func UnmarshalService(data string, s *didcore.Service) error { 242 | propertyMap, err := parseTXTRecordData(data) 243 | if err != nil { 244 | return err 245 | } 246 | for property, v := range propertyMap { 247 | switch property { 248 | case "id": 249 | s.ID = strings.Join(v, "") 250 | case "t": 251 | s.Type = strings.Join(v, "") 252 | case "se": 253 | var validEndpoints []string 254 | for _, uri := range v { 255 | if _, err := url.ParseRequestURI(uri); err != nil { 256 | return errors.New("invalid service endpoint") 257 | } 258 | validEndpoints = append(validEndpoints, uri) 259 | } 260 | s.ServiceEndpoint = validEndpoints 261 | default: 262 | continue 263 | } 264 | } 265 | 266 | return nil 267 | } 268 | 269 | func pluckSort(hayStack []didcore.VerificationMethod) []string { 270 | var ids []string 271 | for _, v := range hayStack { 272 | ids = append(ids, v.ID) 273 | } 274 | sort.Strings(ids) 275 | return ids 276 | } 277 | 278 | // methodsToKeys takes a list of method indices and returns the corresonding verification method _kN keys 279 | func methodsToKeys(methods []string, idToKey map[string]string) []string { 280 | var keys []string 281 | for _, v := range methods { 282 | k, ok := idToKey[v] 283 | if ok { 284 | keys = append(keys, k) 285 | } 286 | } 287 | return keys 288 | } 289 | -------------------------------------------------------------------------------- /dids/diddht/internal/dns/did_test.go: -------------------------------------------------------------------------------- 1 | package dns 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/alecthomas/assert/v2" 8 | "github.com/decentralized-identity/web5-go/dids/didcore" 9 | ) 10 | 11 | func Test_MarshalDIDDocument(t *testing.T) { 12 | 13 | var didDoc didcore.Document 14 | assert.NoError(t, json.Unmarshal([]byte(` 15 | { 16 | "id": "did:dht:cwxob5rbhhu3z9x3gfqy6cthqgm6ngrh4k8s615n7pw11czoq4fy", 17 | "verificationMethod": [ 18 | { 19 | "id": "did:dht:cwxob5rbhhu3z9x3gfqy6cthqgm6ngrh4k8s615n7pw11czoq4fy#0", 20 | "type": "JsonWebKey", 21 | "controller": "did:dht:cwxob5rbhhu3z9x3gfqy6cthqgm6ngrh4k8s615n7pw11czoq4fy", 22 | "publicKeyJwk": { 23 | "crv": "Ed25519", 24 | "kty": "OKP", 25 | "kid": "0", 26 | "x": "ZR8A7IHnJ5v9-TFcDzI8cZfhGJzSj29LYutpKTLwdoo" 27 | } 28 | } 29 | ], 30 | "authentication": [ 31 | "did:dht:cwxob5rbhhu3z9x3gfqy6cthqgm6ngrh4k8s615n7pw11czoq4fy#0" 32 | ], 33 | "assertionMethod": [ 34 | "did:dht:cwxob5rbhhu3z9x3gfqy6cthqgm6ngrh4k8s615n7pw11czoq4fy#0" 35 | ], 36 | "capabilityInvocation": [ 37 | "did:dht:cwxob5rbhhu3z9x3gfqy6cthqgm6ngrh4k8s615n7pw11czoq4fy#0" 38 | ], 39 | "capabilityDelegation": [ 40 | "did:dht:cwxob5rbhhu3z9x3gfqy6cthqgm6ngrh4k8s615n7pw11czoq4fy#0" 41 | ] 42 | } 43 | `), &didDoc)) 44 | 45 | assert.NotZero(t, didDoc.VerificationMethod) 46 | buf, err := MarshalDIDDocument(&didDoc) 47 | assert.NoError(t, err) 48 | 49 | assert.NotZero(t, len(buf)) 50 | 51 | rec, _ := parseDNSDID(buf) 52 | reParsedDoc, err := rec.DIDDocument() 53 | assert.NoError(t, err) 54 | assert.NotZero(t, reParsedDoc) 55 | assert.Equal(t, &didDoc, reParsedDoc) 56 | } 57 | -------------------------------------------------------------------------------- /dids/diddht/internal/dns/dns.go: -------------------------------------------------------------------------------- 1 | package dns 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/decentralized-identity/web5-go/dids/didcore" 9 | "golang.org/x/net/dns/dnsmessage" 10 | ) 11 | 12 | // ttl is the default TTL for DNS records recommended by https://did-dht.com/#note-1 13 | const ttl = 7200 14 | 15 | // decoder is used to structure the DNS representation of a DID 16 | type decoder struct { 17 | // zbase32 encoded id 18 | id string 19 | rootRecord string 20 | records map[string]string 21 | } 22 | 23 | func (rec *decoder) DIDDocument() (*didcore.Document, error) { 24 | if len(rec.rootRecord) == 0 { 25 | return nil, errors.New("no root record found") 26 | } 27 | relationshipMap, err := parseVerificationRelationships(rec.rootRecord) 28 | if err != nil { 29 | return nil, err 30 | } 31 | 32 | // Now we have a did in a dns record. yay 33 | document := &didcore.Document{ 34 | ID: "did:dht:" + rec.id, 35 | } 36 | 37 | // Now create the did document 38 | for name, data := range rec.records { 39 | switch { 40 | case strings.HasPrefix(name, "_k"): 41 | var vMethod didcore.VerificationMethod 42 | if err := UnmarshalVerificationMethod(data, document.ID, &vMethod); err != nil { 43 | // TODO handle error 44 | continue 45 | } 46 | 47 | // TODO somehow we need to keep track of the order - verification method index should keep entryID order 48 | // extracting kN from _kN._did 49 | entryID := strings.Split(name, ".")[0][1:] 50 | relationships, ok := relationshipMap[entryID] 51 | 52 | if !ok { 53 | // no relationships 54 | continue 55 | } 56 | 57 | opts := []didcore.Purpose{} 58 | for _, r := range relationships { 59 | if o, ok := vmPurposeDNStoDID[r]; ok { 60 | opts = append(opts, o) 61 | } 62 | } 63 | 64 | document.AddVerificationMethod( 65 | vMethod, 66 | didcore.Purposes(opts...), 67 | ) 68 | case strings.HasPrefix(name, "_s"): 69 | var service didcore.Service 70 | if err := UnmarshalService(data, &service); err != nil { 71 | // TODO handle error 72 | continue 73 | } 74 | document.AddService(service) 75 | case strings.HasPrefix(name, "_cnt"): 76 | // TODO add controller https://did-dht.com/#controller 77 | // optional field 78 | // comma-separated list of controller DID identifiers 79 | document.Controller = strings.Split(data, ",") 80 | case strings.HasPrefix(name, "_aka"): 81 | // TODO add aka https://did-dht.com/#also-known-as 82 | document.AlsoKnownAs = strings.Split(data, ",") 83 | default: 84 | } 85 | } 86 | 87 | return document, nil 88 | } 89 | 90 | // parseDNSDID takes the bytes of the DNS representation of a DID and creates an internal representation 91 | // used to create a DID document 92 | // TODO move this in it's own internal package 93 | func parseDNSDID(data []byte) (*decoder, error) { 94 | var p dnsmessage.Parser 95 | if _, err := p.Start(data); err != nil { 96 | return nil, err 97 | } 98 | 99 | didRecord := decoder{ 100 | records: make(map[string]string), 101 | } 102 | 103 | // need to skip questions to move the index to the right place to read answers 104 | if err := p.SkipAllQuestions(); err != nil { 105 | return nil, err 106 | } 107 | 108 | for { 109 | h, err := p.AnswerHeader() 110 | if errors.Is(err, dnsmessage.ErrSectionDone) { 111 | break 112 | } 113 | 114 | if h.Type != dnsmessage.TypeTXT { 115 | continue 116 | } 117 | 118 | value, err := p.TXTResource() 119 | if err != nil { 120 | // TODO check what kind of error and see if this should fail 121 | return nil, err 122 | } 123 | 124 | name := h.Name.String() 125 | fullTxtRecord := strings.Join(value.TXT, "") 126 | if strings.HasPrefix(name, "_did") { 127 | didRecord.id = strings.TrimSuffix(strings.TrimPrefix(name, "_did."), ".") 128 | didRecord.rootRecord = fullTxtRecord 129 | continue 130 | } 131 | 132 | didRecord.records[h.Name.String()] = fullTxtRecord 133 | } 134 | 135 | return &didRecord, nil 136 | } 137 | 138 | // TODO on the diddhtrecord we should validate the minimum reqs for a valid did 139 | func parseVerificationRelationships(rootRecord string) (map[string][]string, error) { 140 | rootRecordProps, err := parseTXTRecordData(rootRecord) 141 | if err != nil { 142 | return nil, err 143 | } 144 | // reverse the map to get the relationships 145 | var relationshipMap = make(map[string][]string) 146 | for k, values := range rootRecordProps { 147 | v := strings.Join(values, "") 148 | rel, ok := relationshipMap[v] 149 | if !ok { 150 | rel = []string{} 151 | } 152 | rel = append(rel, k) 153 | relationshipMap[v] = rel 154 | } 155 | 156 | return relationshipMap, nil 157 | } 158 | 159 | func parseTXTRecordData(data string) (map[string][]string, error) { 160 | var result = make(map[string][]string) 161 | fields := strings.Split(data, ";") 162 | if len(fields) == 0 { 163 | return nil, errors.New("no fields found") 164 | } 165 | for _, field := range fields { 166 | kv := strings.Split(field, "=") 167 | if len(kv) != 2 { 168 | return nil, fmt.Errorf("malformed field %s", field) 169 | } 170 | k, v := kv[0], strings.Split(kv[1], ",") 171 | current, ok := result[k] 172 | if ok { 173 | v = append(current, v...) 174 | } 175 | result[k] = v 176 | } 177 | 178 | return result, nil 179 | } 180 | 181 | // newResource creates a new TXT DNS resource with a 7200 TTL 182 | func newResource(name, body string) (dnsmessage.Resource, error) { 183 | headerName, err := dnsmessage.NewName(name) 184 | if err != nil { 185 | return dnsmessage.Resource{}, err 186 | } 187 | return dnsmessage.Resource{ 188 | Header: dnsmessage.ResourceHeader{ 189 | Name: headerName, 190 | Type: dnsmessage.TypeTXT, 191 | TTL: ttl, 192 | }, 193 | Body: &dnsmessage.TXTResource{ 194 | TXT: []string{ 195 | body, 196 | }, 197 | }, 198 | }, nil 199 | } 200 | -------------------------------------------------------------------------------- /dids/diddht/internal/dns/dns_test.go: -------------------------------------------------------------------------------- 1 | package dns 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/alecthomas/assert" 8 | "golang.org/x/net/dns/dnsmessage" 9 | ) 10 | 11 | type DHTTXTResourceOpt func() dnsmessage.Resource 12 | 13 | func WithDNSRecord(name, body string) DHTTXTResourceOpt { 14 | return func() dnsmessage.Resource { 15 | return dnsmessage.Resource{ 16 | Header: dnsmessage.ResourceHeader{ 17 | Name: dnsmessage.MustNewName(name), 18 | Type: dnsmessage.TypeTXT, 19 | TTL: 7200, 20 | }, 21 | Body: &dnsmessage.TXTResource{ 22 | TXT: []string{ 23 | body, 24 | }, 25 | }, 26 | } 27 | } 28 | } 29 | func makeDNSMessage(answersOpt ...DHTTXTResourceOpt) dnsmessage.Message { 30 | 31 | answers := []dnsmessage.Resource{} 32 | for _, a := range answersOpt { 33 | answers = append(answers, a()) 34 | } 35 | 36 | msg := dnsmessage.Message{ 37 | Header: dnsmessage.Header{Response: true, Authoritative: true}, 38 | Answers: answers, 39 | } 40 | 41 | return msg 42 | } 43 | func Test_parseDNSDID(t *testing.T) { 44 | tests := map[string]struct { 45 | msg dnsmessage.Message 46 | expectedError string 47 | assertResult func(t *testing.T, d *decoder) 48 | }{ 49 | "basic did with key": { 50 | msg: makeDNSMessage( 51 | WithDNSRecord("_did.", "vm=k0;auth=k0;asm=k0;inv=k0;del=k0"), 52 | WithDNSRecord("_k0._did.", "id=0;t=0;k=YCcHYL2sYNPDlKaALcEmll2HHyT968M4UWbr-9CFGWE"), 53 | ), 54 | assertResult: func(t *testing.T, d *decoder) { 55 | t.Helper() 56 | assert.False(t, d == nil) 57 | expectedRecords := map[string]string{ 58 | "_k0._did.": "id=0;t=0;k=YCcHYL2sYNPDlKaALcEmll2HHyT968M4UWbr-9CFGWE", 59 | } 60 | assert.Equal(t, "vm=k0;auth=k0;asm=k0;inv=k0;del=k0", d.rootRecord) 61 | assert.True(t, reflect.DeepEqual(expectedRecords, d.records)) 62 | }, 63 | }, 64 | } 65 | 66 | for name, test := range tests { 67 | t.Run(name, func(t *testing.T) { 68 | buf, err := test.msg.Pack() 69 | assert.NoError(t, err) 70 | 71 | dhtDidRecord, err := parseDNSDID(buf) 72 | if test.expectedError != "" { 73 | assert.EqualError(t, err, test.expectedError) 74 | return 75 | } 76 | 77 | assert.NoError(t, err) 78 | assert.Equal(t, "vm=k0;auth=k0;asm=k0;inv=k0;del=k0", dhtDidRecord.rootRecord) 79 | 80 | }) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /dids/diddht/internal/dns/dnsmappings.go: -------------------------------------------------------------------------------- 1 | package dns 2 | 3 | import ( 4 | "github.com/decentralized-identity/web5-go/crypto/dsa" 5 | "github.com/decentralized-identity/web5-go/dids/didcore" 6 | ) 7 | 8 | // vmPurposeDNStoDID maps the DNS representation of the 9 | // verification method relationships to the DID representation. 10 | // 11 | // https://did-dht.com/#verification-relationship-index 12 | var vmPurposeDNStoDID = map[string]didcore.Purpose{ 13 | PurposeAuthentication: didcore.PurposeAuthentication, 14 | PurposeAssertionMethod: didcore.PurposeAssertion, 15 | PurposeKeyAgreement: didcore.PurposeKeyAgreement, 16 | PurposeCapabilityInvocation: didcore.PurposeCapabilityInvocation, 17 | PurposeCapabilityDeletion: didcore.PurposeCapabilityDelegation, 18 | } 19 | 20 | // dhtIndexToAlg maps the DNS representation of the key type index 21 | // to the algorithm ID. 22 | // 23 | // https://did-dht.com/registry/index.html#key-type-index 24 | var dhtIndexToAlg = map[string]string{ 25 | "0": dsa.AlgorithmIDED25519, 26 | "1": dsa.AlgorithmIDSECP256K1, 27 | } 28 | 29 | // algToDhtIndex maps the DID representation of the key type (algorithm) 30 | // to the DNS key type index. 31 | // 32 | // https://did-dht.com/registry/index.html#key-type-index 33 | var algToDhtIndex = map[string]string{ 34 | dsa.AlgorithmIDED25519: "0", 35 | dsa.AlgorithmIDSECP256K1: "1", 36 | } 37 | -------------------------------------------------------------------------------- /dids/diddht/internal/pkarr/gateway.go: -------------------------------------------------------------------------------- 1 | package pkarr 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | "net/url" 9 | "strings" 10 | 11 | "github.com/decentralized-identity/web5-go/dids/diddht/internal/bep44" 12 | ) 13 | 14 | // Client is a client for publishing and fetching BEP44 messages to and from a Pkarr relay server. 15 | type Client struct { 16 | relay string 17 | client *http.Client 18 | } 19 | 20 | // NewClient creates a new Pkarr relay client with the given relay URL and HTTP client. 21 | func NewClient(relay string, client *http.Client) *Client { 22 | return &Client{ 23 | relay: relay, 24 | client: client, 25 | } 26 | } 27 | 28 | // Put Publishes a signed BEP44 message to a Pkarr relay server. 29 | // https://github.com/Nuhvi/pkarr/blob/main/design/relays.md 30 | // 31 | // didID - The DID identifier, used as the key in the DHT; it is the z-base-32 encoding of the Identity Key. 32 | // bep44Message - The BEP44 message to be published, containing the signed DNS packet. 33 | // 34 | // Returns an error if the request fails. 35 | func (r *Client) Put(didID string, msg *bep44.Message) error { 36 | return r.PutWithContext(context.Background(), didID, msg) 37 | } 38 | 39 | // PutWithContext same as put but with context 40 | func (r *Client) PutWithContext(ctx context.Context, didID string, msg *bep44.Message) error { 41 | 42 | // Concatenate the Pkarr relay URL with the identifier to form the full URL. 43 | pkarrURL, err := url.JoinPath(r.relay, didID) 44 | if err != nil { 45 | return err 46 | } 47 | 48 | // Serialize the BEP44 message to a byte slice. 49 | body, err := msg.Marshal() 50 | if err != nil { 51 | return err 52 | } 53 | 54 | req, err := http.NewRequestWithContext(ctx, http.MethodPut, pkarrURL, strings.NewReader(string(body))) 55 | if err != nil { 56 | return err 57 | } 58 | 59 | req.Header.Set("Content-Type", "application/octet-stream") 60 | 61 | // Transmit the Put request to the Pkarr relay and get the response. 62 | res, err := r.client.Do(req) 63 | if err != nil { 64 | return err 65 | } 66 | defer res.Body.Close() 67 | 68 | if res.StatusCode != http.StatusOK { 69 | resBody, err := io.ReadAll(res.Body) 70 | if err != nil { 71 | return err 72 | } 73 | return fmt.Errorf("failed to put message: %s - %s", res.Status, string(resBody)) 74 | } 75 | 76 | // Return `true` if the DHT request was successful, otherwise return `false`. 77 | return nil 78 | } 79 | 80 | // Fetch fetches a signed BEP44 message from a Pkarr relay server. 81 | func (r *Client) Fetch(didID string) (*bep44.Message, error) { 82 | return r.FetchWithContext(context.Background(), didID) 83 | } 84 | 85 | // FetchWithContext fetches a signed BEP44 message from a Pkarr relay server. 86 | func (r *Client) FetchWithContext(ctx context.Context, didID string) (*bep44.Message, error) { 87 | // Concatenate the Pkarr relay URL with the identifier to form the full URL. 88 | pkarrURL, err := url.JoinPath(r.relay, didID) 89 | if err != nil { 90 | // TODO log err 91 | return nil, err 92 | } 93 | 94 | req, err := http.NewRequestWithContext(ctx, http.MethodGet, pkarrURL, nil) 95 | if err != nil { 96 | return nil, err 97 | } 98 | 99 | // Transmit the Get request to the Pkarr relay and get the response. 100 | res, err := r.client.Do(req) 101 | if err != nil { 102 | // TODO log err 103 | return nil, err 104 | } 105 | defer res.Body.Close() 106 | 107 | if res.StatusCode != http.StatusOK { 108 | // TODO log err 109 | return nil, fmt.Errorf("failed to get message: %s", res.Status) 110 | } 111 | 112 | // Read the response body into a byte slice. 113 | body, err := io.ReadAll(res.Body) 114 | if err != nil { 115 | // TODO log err 116 | return nil, err 117 | } 118 | 119 | // Decode the response body into a BEP44 message. 120 | bep44Message := bep44.Message{} 121 | if err := bep44.UnmarshalMessage(body, &bep44Message); err != nil { 122 | // TODO log err 123 | return nil, err 124 | } 125 | 126 | // Return the BEP44 message. 127 | return &bep44Message, nil 128 | } 129 | -------------------------------------------------------------------------------- /dids/diddht/internal/pkarr/gateway_test.go: -------------------------------------------------------------------------------- 1 | package pkarr 2 | -------------------------------------------------------------------------------- /dids/diddht/resolver.go: -------------------------------------------------------------------------------- 1 | package diddht 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | 7 | "github.com/decentralized-identity/web5-go/dids/did" 8 | "github.com/decentralized-identity/web5-go/dids/didcore" 9 | "github.com/decentralized-identity/web5-go/dids/diddht/internal/dns" 10 | "github.com/decentralized-identity/web5-go/dids/diddht/internal/pkarr" 11 | "github.com/tv42/zbase32" 12 | ) 13 | 14 | // DefaultResolver uses the default Pkarr gateway client: https://diddht.tbddev.org 15 | func DefaultResolver() *Resolver { 16 | return &Resolver{ 17 | relay: getDefaultGateway(), 18 | } 19 | } 20 | 21 | // Resolver is a client for resolving DIDs using the DHT network. 22 | type Resolver struct { 23 | relay gateway 24 | } 25 | 26 | // NewResolver creates a new Resolver instance with the given relay and HTTP client. 27 | // TODO make this relay an option and use default relay if not provided 28 | func NewResolver(relayURL string, client *http.Client) *Resolver { 29 | pkarrRelay := pkarr.NewClient(relayURL, client) 30 | return &Resolver{ 31 | relay: pkarrRelay, 32 | } 33 | } 34 | 35 | // Resolve resolves a DID using the DHT method 36 | func (r *Resolver) Resolve(uri string) (didcore.ResolutionResult, error) { 37 | return r.ResolveWithContext(context.Background(), uri) 38 | } 39 | 40 | // ResolveWithContext resolves a DID using the DHT method. This is the context aware version of Resolve. 41 | func (r *Resolver) ResolveWithContext(ctx context.Context, uri string) (didcore.ResolutionResult, error) { 42 | 43 | // 1. Parse URI and make sure it's a DHT method 44 | did, err := did.Parse(uri) 45 | if err != nil { 46 | // TODO log err 47 | return didcore.ResolutionResultWithError("invalidDid"), didcore.ResolutionError{Code: "invalidDid"} 48 | } 49 | 50 | if did.Method != "dht" { 51 | return didcore.ResolutionResultWithError("methodNotSupported"), didcore.ResolutionError{Code: "methodNotSupported"} 52 | } 53 | 54 | // 2. ensure did ID is zbase32 55 | identifier, err := zbase32.DecodeString(did.ID) 56 | if err != nil { 57 | // TODO log err 58 | return didcore.ResolutionResultWithError("invalidPublicKey"), didcore.ResolutionError{Code: "invalidPublicKey"} 59 | } 60 | 61 | if len(identifier) == 0 { 62 | // return nil, fmt.Errorf("no bytes decoded from zbase32 identifier %s", did.ID) 63 | // TODO log err 64 | return didcore.ResolutionResultWithError("invalidPublicKey"), didcore.ResolutionError{Code: "invalidPublicKey"} 65 | } 66 | 67 | // 3. fetch from the relay 68 | bep44Message, err := r.relay.FetchWithContext(ctx, did.ID) 69 | if err != nil { 70 | // TODO log err 71 | return didcore.ResolutionResultWithError("notFound"), didcore.ResolutionError{Code: "notFound"} 72 | } 73 | 74 | // get the dns payload from the bep44 message 75 | bep44MessagePayload := bep44Message.V 76 | document, err := dns.UnmarshalDIDDocument(bep44MessagePayload) 77 | if err != nil { 78 | // TODO log err 79 | return didcore.ResolutionResultWithError("invalidDid"), didcore.ResolutionError{Code: "invalidDid"} 80 | } 81 | 82 | return didcore.ResolutionResultWithDocument(*document), nil 83 | } 84 | -------------------------------------------------------------------------------- /dids/diddht/resolver_test.go: -------------------------------------------------------------------------------- 1 | package diddht 2 | 3 | import ( 4 | "encoding/hex" 5 | "encoding/json" 6 | "net/http" 7 | "net/http/httptest" 8 | "os" 9 | "testing" 10 | 11 | "github.com/alecthomas/assert/v2" 12 | ) 13 | 14 | const dhtSpecVectors string = "../../web5-spec/test-vectors/did_dht/resolve.json" 15 | 16 | type vector struct { 17 | Description string `json:"description"` 18 | Input struct { 19 | DIDUri string `json:"didUri"` 20 | } `json:"input"` 21 | Output struct { 22 | DIDResolutionMetadata struct { 23 | Error string `json:"error"` 24 | } `json:"didResolutionMetadata"` 25 | } `json:"output"` 26 | Errors bool `json:"errors"` 27 | } 28 | 29 | func initVector() ([]vector, error) { 30 | // Load test vectors from file 31 | data, err := os.ReadFile(dhtSpecVectors) 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | // Unmarshal test vectors 37 | vectorData := struct { 38 | Vectors []vector `json:"vectors"` 39 | }{} 40 | if err := json.Unmarshal(data, &vectorData); err != nil { 41 | return nil, err 42 | } 43 | return vectorData.Vectors, nil 44 | } 45 | 46 | func Test_VectorsResolve(t *testing.T) { 47 | vectors, err := initVector() 48 | assert.NoError(t, err) 49 | 50 | mocks := map[string]vector{} 51 | for _, v := range vectors { 52 | mocks[v.Input.DIDUri] = v 53 | } 54 | 55 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 56 | w.WriteHeader(http.StatusNotFound) 57 | return 58 | })) 59 | defer ts.Close() 60 | 61 | r := NewResolver(ts.URL, http.DefaultClient) 62 | 63 | for _, vector := range vectors { 64 | t.Run(vector.Description, func(t *testing.T) { 65 | res, err := r.Resolve(vector.Input.DIDUri) 66 | if vector.Errors { 67 | assert.True(t, err != nil) 68 | assert.Equal(t, res.ResolutionMetadata.Error, vector.Output.DIDResolutionMetadata.Error) 69 | } 70 | }) 71 | } 72 | } 73 | 74 | func Test_resolve(t *testing.T) { 75 | 76 | // vector taken from https://github.com/decentralized-identity/web5-js/blob/91d52aaa9410db5e5f7c3c31ebfe0d4956028496/packages/dids/tests/methods/did-dht.spec.ts#L725 77 | vectors := map[string]string{ 78 | "did:dht:9tjoow45ef1hksoo96bmzkwwy3mhme95d7fsi3ezjyjghmp75qyo": "ea33e704f3a48a3392f54b28744cdfb4e24780699f92ba7df62fd486d2a2cda3f263e1c6bcbd" + 79 | "75d438be7316e5d6e94b13e98151f599cfecefad0b37432bd90a0000000065b0ed1600008400" + 80 | "0000000300000000035f6b30045f6469643439746a6f6f773435656631686b736f6f3936626d" + 81 | "7a6b777779336d686d653935643766736933657a6a796a67686d70373571796f000010000100" + 82 | "001c2000373669643d303b743d303b6b3d5f464d49553174425a63566145502d437536715542" + 83 | "6c66466f5f73665332726c4630675362693239323445045f747970045f6469643439746a6f6f" + 84 | "773435656631686b736f6f3936626d7a6b777779336d686d653935643766736933657a6a796a" + 85 | "67686d70373571796f000010000100001c2000070669643d372c36045f6469643439746a6f6f" + 86 | "773435656631686b736f6f3936626d7a6b777779336d686d653935643766736933657a6a796a" + 87 | "67686d70373571796f000010000100001c20002726763d303b766d3d6b303b617574683d6b30" + 88 | "3b61736d3d6b303b64656c3d6b303b696e763d6b30", 89 | } 90 | 91 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 92 | did := "did:dht:" + r.URL.Path[1:] 93 | defer r.Body.Close() 94 | buf, ok := vectors[did] 95 | if !ok { 96 | w.WriteHeader(http.StatusNotFound) 97 | return 98 | } 99 | data, err := hex.DecodeString(buf) 100 | assert.NoError(t, err) 101 | _, err = w.Write(data) 102 | assert.NoError(t, err) 103 | 104 | })) 105 | defer ts.Close() 106 | 107 | r := NewResolver(ts.URL, http.DefaultClient) 108 | 109 | for did := range vectors { 110 | t.Run(did, func(t *testing.T) { 111 | res, err := r.Resolve(did) 112 | assert.NoError(t, err) 113 | assert.NotZero(t, res.Document) 114 | assert.Equal(t, res.Document.ID, did) 115 | }) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /dids/didjwk/didjwk.go: -------------------------------------------------------------------------------- 1 | package didjwk 2 | 3 | import ( 4 | "context" 5 | "encoding/base64" 6 | "encoding/json" 7 | "fmt" 8 | 9 | "github.com/decentralized-identity/web5-go/crypto" 10 | "github.com/decentralized-identity/web5-go/crypto/dsa" 11 | "github.com/decentralized-identity/web5-go/dids/did" 12 | "github.com/decentralized-identity/web5-go/dids/didcore" 13 | "github.com/decentralized-identity/web5-go/jwk" 14 | ) 15 | 16 | // createOptions is a struct that contains all options that can be passed to [Create] 17 | type createOptions struct { 18 | keyManager crypto.KeyManager 19 | algorithmID string 20 | } 21 | 22 | // CreateOption is a type returned by all [Create] options for variadic parameter support 23 | type CreateOption func(o *createOptions) 24 | 25 | // KeyManager is an option that can be passed to Create to provide a KeyManager 26 | func KeyManager(k crypto.KeyManager) CreateOption { 27 | return func(o *createOptions) { 28 | o.keyManager = k 29 | } 30 | } 31 | 32 | // AlgorithmID is an option that can be passed to Create to specify a specific 33 | // cryptographic algorithm to use to generate the private key 34 | func AlgorithmID(id string) CreateOption { 35 | return func(o *createOptions) { 36 | o.algorithmID = id 37 | } 38 | } 39 | 40 | // Create can be used to create a new `did:jwk`. `did:jwk` is useful in scenarios where: 41 | // - Offline resolution is preferred 42 | // - Key rotation is not required 43 | // - Service endpoints are not necessary 44 | // 45 | // Spec: https://github.com/quartzjer/did-jwk/blob/main/spec.md 46 | func Create(opts ...CreateOption) (did.BearerDID, error) { 47 | o := createOptions{ 48 | keyManager: crypto.NewLocalKeyManager(), 49 | algorithmID: dsa.AlgorithmIDED25519, 50 | } 51 | 52 | for _, opt := range opts { 53 | opt(&o) 54 | } 55 | 56 | keyMgr := o.keyManager 57 | 58 | keyID, err := keyMgr.GeneratePrivateKey(o.algorithmID) 59 | if err != nil { 60 | return did.BearerDID{}, fmt.Errorf("failed to generate private key: %w", err) 61 | } 62 | 63 | publicJWK, _ := keyMgr.GetPublicKey(keyID) 64 | bytes, err := json.Marshal(publicJWK) 65 | if err != nil { 66 | return did.BearerDID{}, fmt.Errorf("failed to marshal public key: %w", err) 67 | } 68 | 69 | id := base64.RawURLEncoding.EncodeToString(bytes) 70 | 71 | didJWK := did.DID{ 72 | Method: "jwk", 73 | URI: "did:jwk:" + id, 74 | ID: id, 75 | } 76 | 77 | bearerDID := did.BearerDID{ 78 | DID: didJWK, 79 | KeyManager: keyMgr, 80 | Document: createDocument(didJWK, publicJWK), 81 | } 82 | 83 | return bearerDID, nil 84 | } 85 | 86 | // Resolver is a type to implement resolution 87 | type Resolver struct{} 88 | 89 | // ResolveWithContext the provided DID URI (must be a did:jwk) as per the wee bit of detail provided in the 90 | // spec: https://github.com/quartzjer/did-jwk/blob/main/spec.md 91 | func (r Resolver) ResolveWithContext(ctx context.Context, uri string) (didcore.ResolutionResult, error) { 92 | return r.Resolve(uri) 93 | } 94 | 95 | // Resolve the provided DID URI (must be a did:jwk) as per the wee bit of detail provided in the 96 | // spec: https://github.com/quartzjer/did-jwk/blob/main/spec.md 97 | func (r Resolver) Resolve(uri string) (didcore.ResolutionResult, error) { 98 | did, err := did.Parse(uri) 99 | if err != nil { 100 | return didcore.ResolutionResultWithError("invalidDid"), didcore.ResolutionError{Code: "invalidDid"} 101 | } 102 | 103 | if did.Method != "jwk" { 104 | return didcore.ResolutionResultWithError("invalidDid"), didcore.ResolutionError{Code: "invalidDid"} 105 | } 106 | 107 | decodedID, err := base64.RawURLEncoding.DecodeString(did.ID) 108 | if err != nil { 109 | return didcore.ResolutionResultWithError("invalidDid"), didcore.ResolutionError{Code: "invalidDid"} 110 | } 111 | 112 | var jwk jwk.JWK 113 | err = json.Unmarshal(decodedID, &jwk) 114 | if err != nil { 115 | return didcore.ResolutionResultWithError("invalidDid"), didcore.ResolutionError{Code: "invalidDid"} 116 | } 117 | 118 | doc := createDocument(did, jwk) 119 | return didcore.ResolutionResultWithDocument(doc), nil 120 | } 121 | 122 | func createDocument(did did.DID, publicKey jwk.JWK) didcore.Document { 123 | doc := didcore.Document{ 124 | Context: []string{"https://www.w3.org/ns/did/v1"}, 125 | ID: did.URI, 126 | } 127 | 128 | vm := didcore.VerificationMethod{ 129 | ID: did.URI + "#0", 130 | Type: "JsonWebKey", 131 | Controller: did.URI, 132 | PublicKeyJwk: &publicKey, 133 | } 134 | 135 | doc.AddVerificationMethod( 136 | vm, 137 | didcore.Purposes("assertionMethod", "authentication", "capabilityInvocation", "capabilityDelegation"), 138 | ) 139 | 140 | return doc 141 | } 142 | -------------------------------------------------------------------------------- /dids/didjwk/didjwk_test.go: -------------------------------------------------------------------------------- 1 | package didjwk_test 2 | 3 | import ( 4 | "fmt" 5 | "github.com/decentralized-identity/web5-go/dids/did" 6 | "testing" 7 | 8 | "github.com/alecthomas/assert/v2" 9 | "github.com/decentralized-identity/web5-go" 10 | "github.com/decentralized-identity/web5-go/dids/didcore" 11 | "github.com/decentralized-identity/web5-go/dids/didjwk" 12 | "github.com/decentralized-identity/web5-go/jwk" 13 | ) 14 | 15 | func TestCreate(t *testing.T) { 16 | did, err := didjwk.Create() 17 | assert.NoError(t, err) 18 | 19 | assert.Equal(t, "jwk", did.Method) 20 | assert.True(t, did.Fragment == "", "expected fragment to be empty") 21 | assert.True(t, did.Path == "", "expected path to be empty") 22 | assert.True(t, did.Query == "", "expected query to be empty") 23 | assert.True(t, did.ID != "", "expected id to be non-empty") 24 | assert.Equal(t, "did:jwk:"+did.ID, did.URI) 25 | } 26 | 27 | func TestParse(t *testing.T) { 28 | source, err := didjwk.Create() 29 | assert.NoError(t, err) 30 | original := source.DID 31 | 32 | // URI -> Parse 33 | parseURIDID, err := did.Parse(original.URI) 34 | assert.NoError(t, err) 35 | assert.Equal(t, original, parseURIDID) 36 | 37 | // String -> Parse 38 | parseStringDID, err := did.Parse(original.String()) 39 | assert.NoError(t, err) 40 | assert.Equal(t, original, parseStringDID) 41 | 42 | // Value -> Scan 43 | var scanDID did.DID 44 | value, err := original.Value() 45 | assert.NoError(t, err) 46 | err = scanDID.Scan(value) 47 | assert.NoError(t, err) 48 | assert.Equal(t, original, scanDID) 49 | } 50 | 51 | func TestResolveDIDJWK(t *testing.T) { 52 | resolver := &didjwk.Resolver{} 53 | result, err := resolver.Resolve("did:jwk:eyJraWQiOiJ1cm46aWV0ZjpwYXJhbXM6b2F1dGg6andrLXRodW1icHJpbnQ6c2hhLTI1NjpGZk1iek9qTW1RNGVmVDZrdndUSUpqZWxUcWpsMHhqRUlXUTJxb2JzUk1NIiwia3R5IjoiT0tQIiwiY3J2IjoiRWQyNTUxOSIsImFsZyI6IkVkRFNBIiwieCI6IkFOUmpIX3p4Y0tCeHNqUlBVdHpSYnA3RlNWTEtKWFE5QVBYOU1QMWo3azQifQ") 54 | assert.NoError(t, err) 55 | assert.Equal(t, 1, len(result.Document.VerificationMethod)) 56 | 57 | vm := result.Document.VerificationMethod[0] 58 | assert.True(t, vm != didcore.VerificationMethod{}, "expected verification method to be non-empty") 59 | assert.NotEqual[jwk.JWK](t, *vm.PublicKeyJwk, jwk.JWK{}, "expected publicKeyJwk to be non-empty") 60 | 61 | assert.Equal(t, 1, len(result.Document.Authentication)) 62 | assert.Equal(t, 1, len(result.Document.AssertionMethod)) 63 | } 64 | 65 | func TestVector_Resolve(t *testing.T) { 66 | testVectors, err := 67 | web5.LoadTestVectors[string, didcore.ResolutionResult]("../../web5-spec/test-vectors/did_jwk/resolve.json") 68 | assert.NoError(t, err) 69 | fmt.Println("Running test vectors: ", testVectors.Description) 70 | 71 | resolver := didjwk.Resolver{} 72 | 73 | for _, vector := range testVectors.Vectors { 74 | t.Run(vector.Description, func(t *testing.T) { 75 | fmt.Println("Running test vector: ", vector.Description) 76 | 77 | result, err := resolver.Resolve(vector.Input) 78 | 79 | if vector.Errors { 80 | assert.Error(t, err) 81 | } else { 82 | assert.NoError(t, err) 83 | assert.Equal(t, vector.Output, result) 84 | } 85 | }) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /dids/didweb/didweb_test.go: -------------------------------------------------------------------------------- 1 | package didweb_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/alecthomas/assert/v2" 7 | "github.com/decentralized-identity/web5-go/dids/did" 8 | "github.com/decentralized-identity/web5-go/dids/didcore" 9 | "github.com/decentralized-identity/web5-go/dids/didweb" 10 | ) 11 | 12 | func TestCreate(t *testing.T) { 13 | bearerDID, err := didweb.Create("localhost:8080") 14 | assert.NoError(t, err) 15 | 16 | assert.NotEqual(t, didcore.Document{}, bearerDID.Document) 17 | 18 | document := bearerDID.Document 19 | assert.Equal(t, "did:web:localhost%3A8080", document.ID) 20 | assert.Equal(t, 1, len(document.VerificationMethod)) 21 | 22 | did := bearerDID.DID 23 | assert.Equal(t, "web", did.Method) 24 | assert.Equal(t, "localhost%3A8080", did.ID) 25 | assert.Equal(t, "did:web:localhost%3A8080", did.URI) 26 | assert.Equal(t, "did:web:localhost%3A8080", did.URL()) 27 | } 28 | 29 | func TestParse(t *testing.T) { 30 | bearerDID, err := didweb.Create("localhost:8080") 31 | assert.NoError(t, err) 32 | original := bearerDID.DID 33 | 34 | // URI -> Parse 35 | parseURIDID, err := did.Parse(original.URI) 36 | assert.NoError(t, err) 37 | assert.Equal(t, original, parseURIDID) 38 | 39 | // String -> Parse 40 | parseStringDID, err := did.Parse(original.String()) 41 | assert.NoError(t, err) 42 | assert.Equal(t, original, parseStringDID) 43 | 44 | // Value -> Scan 45 | var scanDID did.DID 46 | value, err := original.Value() 47 | assert.NoError(t, err) 48 | err = scanDID.Scan(value) 49 | assert.NoError(t, err) 50 | assert.Equal(t, original, scanDID) 51 | } 52 | 53 | func TestCreate_WithOptions(t *testing.T) { 54 | bearerDID, err := didweb.Create( 55 | "localhost:8080", 56 | didweb.Service("pfi", "PFI", "http://localhost:8080/tbdex"), 57 | didweb.Service("idv", "IDV", "http://localhost:8080/idv"), 58 | didweb.AlsoKnownAs("did:example:123"), 59 | didweb.Controllers("did:example:123"), 60 | ) 61 | 62 | assert.NoError(t, err) 63 | assert.NotEqual(t, did.BearerDID{}, bearerDID) 64 | 65 | document := bearerDID.Document 66 | assert.Equal(t, 2, len(document.Service)) 67 | 68 | pfisvc := document.Service[0] 69 | assert.NotEqual(t, didcore.Service{}, pfisvc) 70 | assert.Equal(t, "#pfi", pfisvc.ID) 71 | assert.Equal(t, "PFI", pfisvc.Type) 72 | assert.Equal(t, "http://localhost:8080/tbdex", pfisvc.ServiceEndpoint[0]) 73 | 74 | idvsvc := document.Service[1] 75 | assert.NotEqual(t, didcore.Service{}, idvsvc) 76 | assert.Equal(t, "#idv", idvsvc.ID) 77 | assert.Equal(t, "IDV", idvsvc.Type) 78 | assert.Equal(t, "http://localhost:8080/idv", idvsvc.ServiceEndpoint[0]) 79 | 80 | assert.Equal(t, "did:example:123", document.AlsoKnownAs[0]) 81 | assert.Equal(t, "did:example:123", document.Controller[0]) 82 | 83 | assert.Equal(t, 1, len(document.VerificationMethod)) 84 | assert.Contains(t, document.VerificationMethod[0].ID, document.ID) 85 | 86 | } 87 | 88 | func TestTransformID(t *testing.T) { 89 | var vectors = []struct { 90 | input string 91 | output string 92 | err bool 93 | }{ 94 | { 95 | input: "example.com:user:alice", 96 | output: "https://example.com/user/alice/did.json", 97 | err: false, 98 | }, 99 | { 100 | input: "localhost%3A8080:user:alice", 101 | output: "http://localhost:8080/user/alice/did.json", 102 | err: false, 103 | }, 104 | { 105 | input: "192.168.1.100%3A8892:ingress", 106 | output: "http://192.168.1.100:8892/ingress/did.json", 107 | err: false, 108 | }, 109 | { 110 | input: "www.linkedin.com", 111 | output: "https://www.linkedin.com/.well-known/did.json", 112 | err: false, 113 | }, 114 | } 115 | 116 | for _, v := range vectors { 117 | t.Run(v.input, func(t *testing.T) { 118 | output, err := didweb.TransformID(v.input) 119 | assert.NoError(t, err) 120 | assert.Equal(t, v.output, output) 121 | }) 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /dids/resolver.go: -------------------------------------------------------------------------------- 1 | package dids 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | 7 | "github.com/decentralized-identity/web5-go/dids/did" 8 | "github.com/decentralized-identity/web5-go/dids/didcore" 9 | "github.com/decentralized-identity/web5-go/dids/diddht" 10 | "github.com/decentralized-identity/web5-go/dids/didjwk" 11 | "github.com/decentralized-identity/web5-go/dids/didweb" 12 | ) 13 | 14 | // Resolve resolves the provided DID URI. This function is capable of resolving 15 | // the DID methods implemented in web5-go 16 | func Resolve(uri string) (didcore.ResolutionResult, error) { 17 | return getDefaultResolver().Resolve(uri) 18 | } 19 | 20 | // ResolveWithContext resolves the provided DID URI. This function is capable of resolving 21 | // the DID methods implemented in web5-go 22 | func ResolveWithContext(ctx context.Context, uri string) (didcore.ResolutionResult, error) { 23 | return getDefaultResolver().ResolveWithContext(ctx, uri) 24 | } 25 | 26 | var instance *didResolver 27 | var once sync.Once 28 | 29 | func getDefaultResolver() *didResolver { 30 | once.Do(func() { 31 | instance = &didResolver{ 32 | resolvers: map[string]didcore.MethodResolver{ 33 | "dht": diddht.DefaultResolver(), 34 | "jwk": didjwk.Resolver{}, 35 | "web": didweb.Resolver{}, 36 | }, 37 | } 38 | }) 39 | 40 | return instance 41 | } 42 | 43 | type didResolver struct { 44 | resolvers map[string]didcore.MethodResolver 45 | } 46 | 47 | func (r *didResolver) Resolve(uri string) (didcore.ResolutionResult, error) { 48 | did, err := did.Parse(uri) 49 | if err != nil { 50 | return didcore.ResolutionResultWithError("invalidDid"), didcore.ResolutionError{Code: "invalidDid"} 51 | } 52 | 53 | resolver := r.resolvers[did.Method] 54 | if resolver == nil { 55 | return didcore.ResolutionResultWithError("methodNotSupported"), didcore.ResolutionError{Code: "methodNotSupported"} 56 | } 57 | 58 | return resolver.Resolve(uri) 59 | } 60 | 61 | func (r *didResolver) ResolveWithContext(ctx context.Context, uri string) (didcore.ResolutionResult, error) { 62 | did, err := did.Parse(uri) 63 | if err != nil { 64 | return didcore.ResolutionResultWithError("invalidDid"), didcore.ResolutionError{Code: "invalidDid"} 65 | } 66 | 67 | resolver := r.resolvers[did.Method] 68 | if resolver == nil { 69 | return didcore.ResolutionResultWithError("methodNotSupported"), didcore.ResolutionError{Code: "methodNotSupported"} 70 | } 71 | 72 | return resolver.ResolveWithContext(ctx, uri) 73 | } 74 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/decentralized-identity/web5-go 2 | 3 | go 1.22.0 4 | 5 | require ( 6 | github.com/PaesslerAG/jsonpath v0.1.1 7 | github.com/alecthomas/assert v1.0.0 8 | github.com/alecthomas/assert/v2 v2.5.0 9 | github.com/alecthomas/kong v0.8.1 10 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 11 | github.com/google/uuid v1.6.0 12 | github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 13 | github.com/stretchr/testify v1.9.0 14 | github.com/tv42/zbase32 v0.0.0-20220222190657-f76a9fc892fa 15 | golang.org/x/net v0.24.0 16 | ) 17 | 18 | require ( 19 | github.com/PaesslerAG/gval v1.0.0 // indirect 20 | github.com/alecthomas/colour v0.1.0 // indirect 21 | github.com/alecthomas/repr v0.3.0 // indirect 22 | github.com/davecgh/go-spew v1.1.1 // indirect 23 | github.com/hexops/gotextdiff v1.0.3 // indirect 24 | github.com/mattn/go-isatty v0.0.20 // indirect 25 | github.com/pmezard/go-difflib v1.0.0 // indirect 26 | github.com/sergi/go-diff v1.3.1 // indirect 27 | golang.org/x/sys v0.19.0 // indirect 28 | gopkg.in/yaml.v3 v3.0.1 // indirect 29 | ) 30 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/PaesslerAG/gval v1.0.0 h1:GEKnRwkWDdf9dOmKcNrar9EA1bz1z9DqPIO1+iLzhd8= 2 | github.com/PaesslerAG/gval v1.0.0/go.mod h1:y/nm5yEyTeX6av0OfKJNp9rBNj2XrGhAf5+v24IBN1I= 3 | github.com/PaesslerAG/jsonpath v0.1.0/go.mod h1:4BzmtoM/PI8fPO4aQGIusjGxGir2BzcV0grWtFzq1Y8= 4 | github.com/PaesslerAG/jsonpath v0.1.1 h1:c1/AToHQMVsduPAa4Vh6xp2U0evy4t8SWp8imEsylIk= 5 | github.com/PaesslerAG/jsonpath v0.1.1/go.mod h1:lVboNxFGal/VwW6d9JzIy56bUsYAP6tH/x80vjnCseY= 6 | github.com/alecthomas/assert v1.0.0 h1:3XmGh/PSuLzDbK3W2gUbRXwgW5lqPkuqvRgeQ30FI5o= 7 | github.com/alecthomas/assert v1.0.0/go.mod h1:va/d2JC+M7F6s+80kl/R3G7FUiW6JzUO+hPhLyJ36ZY= 8 | github.com/alecthomas/assert/v2 v2.5.0 h1:OJKYg53BQx06/bMRBSPDCO49CbCDNiUQXwdoNrt6x5w= 9 | github.com/alecthomas/assert/v2 v2.5.0/go.mod h1:fw5suVxB+wfYJ3291t0hRTqtGzFYdSwstnRQdaQx2DM= 10 | github.com/alecthomas/colour v0.1.0 h1:nOE9rJm6dsZ66RGWYSFrXw461ZIt9A6+nHgL7FRrDUk= 11 | github.com/alecthomas/colour v0.1.0/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0= 12 | github.com/alecthomas/kong v0.8.1 h1:acZdn3m4lLRobeh3Zi2S2EpnXTd1mOL6U7xVml+vfkY= 13 | github.com/alecthomas/kong v0.8.1/go.mod h1:n1iCIO2xS46oE8ZfYCNDqdR0b0wZNrXAIAqro/2132U= 14 | github.com/alecthomas/repr v0.3.0 h1:NeYzUPfjjlqHY4KtzgKJiWd6sVq2eNUPTi34PiFGjY8= 15 | github.com/alecthomas/repr v0.3.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= 16 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 17 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 18 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 19 | github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y= 20 | github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= 21 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= 22 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= 23 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 24 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 25 | github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= 26 | github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= 27 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 28 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 29 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 30 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 31 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 32 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 33 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 34 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 35 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 36 | github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4= 37 | github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY= 38 | github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= 39 | github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= 40 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 41 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 42 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 43 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 44 | github.com/tv42/zbase32 v0.0.0-20220222190657-f76a9fc892fa h1:2EwhXkNkeMjX9iFYGWLPQLPhw9O58BhnYgtYKeqybcY= 45 | github.com/tv42/zbase32 v0.0.0-20220222190657-f76a9fc892fa/go.mod h1:is48sjgBanWcA5CQrPBu9Y5yABY/T2awj/zI65bq704= 46 | golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= 47 | golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= 48 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 49 | golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= 50 | golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 51 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 52 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 53 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 54 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 55 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 56 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 57 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 58 | -------------------------------------------------------------------------------- /jwk/jwk.go: -------------------------------------------------------------------------------- 1 | // Package jwk implements a subset of the JSON Web Key spec (https://tools.ietf.org/html/rfc7517) 2 | package jwk 3 | 4 | import ( 5 | "crypto/sha256" 6 | "encoding/base64" 7 | "encoding/json" 8 | ) 9 | 10 | // JWK represents a JSON Web Key as per RFC7517 (https://tools.ietf.org/html/rfc7517) 11 | // Note that this is a subset of the spec. There are a handful of properties that the 12 | // spec allows for that are not represented here at the moment. This is because we 13 | // only need a subset of the spec for our purposes. 14 | type JWK struct { 15 | ALG string `json:"alg,omitempty"` 16 | KTY string `json:"kty,omitempty"` 17 | CRV string `json:"crv,omitempty"` 18 | D string `json:"d,omitempty"` 19 | X string `json:"x,omitempty"` 20 | Y string `json:"y,omitempty"` 21 | } 22 | 23 | // ComputeThumbprint computes the JWK thumbprint as per RFC7638 (https://tools.ietf.org/html/rfc7638) 24 | func (j JWK) ComputeThumbprint() (string, error) { 25 | thumbprintPayload := map[string]interface{}{ 26 | "crv": j.CRV, 27 | "kty": j.KTY, 28 | "x": j.X, 29 | } 30 | 31 | if j.Y != "" { 32 | thumbprintPayload["y"] = j.Y 33 | } 34 | 35 | bytes, err := json.Marshal(thumbprintPayload) 36 | if err != nil { 37 | return "", err 38 | } 39 | 40 | digest := sha256.Sum256(bytes) 41 | thumbprint := base64.RawURLEncoding.EncodeToString(digest[:]) 42 | 43 | return thumbprint, nil 44 | } 45 | -------------------------------------------------------------------------------- /jwk/jwk_test.go: -------------------------------------------------------------------------------- 1 | package jwk_test 2 | -------------------------------------------------------------------------------- /jws/README.md: -------------------------------------------------------------------------------- 1 | # `jws` 2 | 3 | 4 | # Table of Contents 5 | - [Features](#features) 6 | - [Usage](#usage) 7 | - [Signing:](#signing) 8 | - [Detached Content](#detached-content) 9 | - [Verifying](#verifying) 10 | - [Directory Structure](#directory-structure) 11 | - [Rationale](#rationale) 12 | 13 | 14 | # Features 15 | * Signing a JWS (JSON Web Signature) with a DID 16 | * Verifying a JWS with a DID 17 | 18 | # Usage 19 | 20 | ## Signing: 21 | 22 | ```go 23 | package main 24 | 25 | import ( 26 | "fmt" 27 | "github.com/decentralized-identity/web5-go/didjwk" 28 | "github.com/decentralized-identity/web5-go/jws" 29 | ) 30 | 31 | func main() { 32 | did, err := didjwk.Create() 33 | if err != nil { 34 | fmt.Printf("failed to create did: %v", err) 35 | return 36 | } 37 | 38 | payload := map[string]interface{}{"hello": "world"} 39 | 40 | compactJWS, err := jws.Sign(payload, did) 41 | if err != nil { 42 | fmt.Printf("failed to sign: %v", err) 43 | return 44 | } 45 | 46 | fmt.Printf("compact JWS: %s", compactJWS) 47 | } 48 | ``` 49 | 50 | ## Detached Content 51 | 52 | returning a JWS with detached content can be done like so: 53 | 54 | ```go 55 | package main 56 | 57 | import ( 58 | "fmt" 59 | "github.com/decentralized-identity/web5-go/didjwk" 60 | "github.com/decentralized-identity/web5-go/jws" 61 | ) 62 | 63 | func main() { 64 | did, err := didjwk.Create() 65 | if err != nil { 66 | fmt.Printf("failed to create did: %v", err) 67 | return 68 | } 69 | 70 | payload := map[string]interface{}{"hello": "world"} 71 | 72 | compactJWS, err := jws.Sign(payload, did, Detached(true)) 73 | if err != nil { 74 | fmt.Printf("failed to sign: %v", err) 75 | return 76 | } 77 | 78 | fmt.Printf("compact JWS: %s", compactJWS) 79 | } 80 | ``` 81 | 82 | specifying a specific category of key associated with the provided did to sign with can be done like so: 83 | 84 | ```go 85 | package main 86 | 87 | import ( 88 | "fmt" 89 | "github.com/decentralized-identity/web5-go/didjwk" 90 | "github.com/decentralized-identity/web5-go/jws" 91 | ) 92 | 93 | func main() { 94 | bearerDID, err := didjwk.Create() 95 | if err != nil { 96 | fmt.Printf("failed to create did: %v", err) 97 | return 98 | } 99 | 100 | payload := map[string]interface{}{"hello": "world"} 101 | 102 | compactJWS, err := jws.Sign(payload, did, Purpose("authentication")) 103 | if err != nil { 104 | fmt.Printf("failed to sign: %v", err) 105 | } 106 | 107 | fmt.Printf("compact JWS: %s", compactJWS) 108 | } 109 | ``` 110 | 111 | 112 | ## Verifying 113 | 114 | ```go 115 | package main 116 | 117 | import ( 118 | "fmt" 119 | "github.com/decentralized-identity/web5-go/didjwk" 120 | "github.com/decentralized-identity/web5-go/jws" 121 | ) 122 | 123 | func main() { 124 | compactJWS := "SOME_JWS" 125 | ok, err := jws.Verify(compactJWS) 126 | if (err != nil) { 127 | fmt.Printf("failed to verify JWS: %v", err) 128 | } 129 | 130 | if (!ok) { 131 | fmt.Errorf("integrity check failed") 132 | } 133 | } 134 | ``` 135 | 136 | > [!NOTE] 137 | > an error is returned if something in the process of verification failed whereas `!ok` means the signature is actually shot 138 | 139 | 140 | ## Directory Structure 141 | 142 | ``` 143 | jws 144 | ├── jws.go 145 | └── jws_test.go 146 | ``` 147 | 148 | ### Rationale 149 | bc i wanted `jws.Sign` and `jws.Verify` hipster vibes 150 | -------------------------------------------------------------------------------- /jwt/README.md: -------------------------------------------------------------------------------- 1 | # `jwt` 2 | 3 | # Table of Contents 4 | 5 | - [Table of Contents](#table-of-contents) 6 | - [Usage](#usage) 7 | - [Signing](#signing) 8 | - [Verifying](#verifying) 9 | - [Directory Structure](#directory-structure) 10 | - [Rationale](#rationale) 11 | 12 | 13 | # Usage 14 | 15 | ## Signing 16 | 17 | ```go 18 | package main 19 | 20 | import ( 21 | "fmt" 22 | "github.com/decentralized-identity/web5-go/didjwk" 23 | "github.com/decentralized-identity/web5-go/jwt" 24 | ) 25 | 26 | func main() { 27 | did, err := didjwk.Create() 28 | if err != nil { 29 | panic(err) 30 | } 31 | 32 | claims := jwt.Claims{ 33 | Issuer: did.URI, 34 | Misc: map[string]interface{}{"c_nonce": "abcd123"}, 35 | } 36 | 37 | jwt, err := jwt.Sign(claims, did) 38 | if err != nil { 39 | panic(err) 40 | } 41 | } 42 | ``` 43 | 44 | 45 | ## Verifying 46 | ```go 47 | package main 48 | 49 | import ( 50 | "fmt" 51 | "github.com/decentralized-identity/web5-go/dids" 52 | "github.com/decentralized-identity/web5-go/jwt" 53 | ) 54 | 55 | func main() { 56 | someJWT := "SOME_JWT" 57 | ok, err := jwt.Verify(signedJWT) 58 | if err != nil { 59 | panic(err) 60 | } 61 | 62 | if (!ok) { 63 | fmt.Printf("dookie JWT") 64 | } 65 | } 66 | ``` 67 | 68 | specifying a specific category of key to use relative to the did provided can be done in the same way shown with `jws.Sign` 69 | 70 | # Directory Structure 71 | 72 | ``` 73 | jwt 74 | ├── jwt.go 75 | └── jwt_test.go 76 | ``` 77 | 78 | ### Rationale 79 | same as `jws`. -------------------------------------------------------------------------------- /jwt/jwt_test.go: -------------------------------------------------------------------------------- 1 | package jwt_test 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/alecthomas/assert/v2" 9 | "github.com/decentralized-identity/web5-go/dids/didjwk" 10 | "github.com/decentralized-identity/web5-go/jws" 11 | "github.com/decentralized-identity/web5-go/jwt" 12 | ) 13 | 14 | func TestClaims_MarshalJSON(t *testing.T) { 15 | claims := jwt.Claims{ 16 | Issuer: "issuer", 17 | Misc: map[string]interface{}{"foo": "bar"}, 18 | } 19 | 20 | b, err := json.Marshal(&claims) 21 | assert.NoError(t, err) 22 | 23 | obj := make(map[string]interface{}) 24 | err = json.Unmarshal(b, &obj) 25 | assert.NoError(t, err) 26 | 27 | assert.Equal(t, "issuer", obj["iss"]) 28 | assert.False(t, obj["foo"] == nil) 29 | } 30 | 31 | func TestClaims_UnmarshalJSON(t *testing.T) { 32 | claims := jwt.Claims{ 33 | Issuer: "issuer", 34 | Misc: map[string]interface{}{"foo": "bar"}, 35 | } 36 | 37 | b, err := json.Marshal(&claims) 38 | assert.NoError(t, err) 39 | 40 | claimsAgane := jwt.Claims{} 41 | err = json.Unmarshal(b, &claimsAgane) 42 | assert.NoError(t, err) 43 | 44 | assert.Equal(t, claims.Issuer, claimsAgane.Issuer) 45 | assert.False(t, claimsAgane.Misc["foo"] == nil) 46 | assert.Equal(t, claimsAgane.Misc["foo"], claims.Misc["foo"]) 47 | } 48 | 49 | func TestSign(t *testing.T) { 50 | did, err := didjwk.Create() 51 | assert.NoError(t, err) 52 | 53 | claims := jwt.Claims{ 54 | Issuer: did.ID, 55 | Misc: map[string]interface{}{"c_nonce": "abcd123"}, 56 | } 57 | 58 | jwt, err := jwt.Sign(claims, did) 59 | assert.NoError(t, err) 60 | 61 | assert.False(t, jwt == "", "expected jwt to not be empty") 62 | } 63 | 64 | func TestSign_IssuerOverridden(t *testing.T) { 65 | did, err := didjwk.Create() 66 | assert.NoError(t, err) 67 | 68 | claims := jwt.Claims{ 69 | Issuer: "something-not-equal-to-did.URI", // this will be overridden by the call to jwt.Sign() 70 | Misc: map[string]interface{}{"c_nonce": "abcd123"}, 71 | } 72 | 73 | signed, err := jwt.Sign(claims, did) 74 | assert.NoError(t, err) 75 | 76 | decoded, err := jwt.Decode(signed) 77 | assert.NoError(t, err) 78 | 79 | assert.Equal(t, did.URI, decoded.Claims.Issuer) 80 | } 81 | 82 | func TestVerify(t *testing.T) { 83 | did, err := didjwk.Create() 84 | assert.NoError(t, err) 85 | 86 | claims := jwt.Claims{ 87 | Issuer: did.URI, 88 | Misc: map[string]interface{}{"c_nonce": "abcd123"}, 89 | } 90 | 91 | signedJWT, err := jwt.Sign(claims, did) 92 | assert.NoError(t, err) 93 | 94 | assert.False(t, signedJWT == "", "expected jwt to not be empty") 95 | 96 | decoded, err := jwt.Verify(signedJWT) 97 | assert.NoError(t, err) 98 | assert.NotEqual(t, decoded, jwt.Decoded{}, "expected decoded to not be empty") 99 | } 100 | 101 | func TestVerify_BadClaims(t *testing.T) { 102 | okHeader, err := jws.Header{ALG: "ES256K", KID: "did:web:abc#key-1"}.Encode() 103 | assert.NoError(t, err) 104 | 105 | input := fmt.Sprintf("%s.%s.%s", okHeader, "hehe", "hehe") 106 | 107 | decoded, err := jwt.Verify(input) 108 | assert.Error(t, err) 109 | assert.Equal(t, jwt.Decoded{}, decoded) 110 | } 111 | 112 | func Test_Decode_Empty(t *testing.T) { 113 | decoded, err := jwt.Decode("") 114 | assert.Error(t, err) 115 | assert.Equal(t, jwt.Decoded{}, decoded) 116 | } 117 | 118 | func Test_Decode_Works(t *testing.T) { 119 | vcJwt := `eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpqd2s6ZXlKcmRIa2lPaUpQUzFBaUxDSmpjbllpT2lKRlpESTFOVEU1SWl3aWVDSTZJbkY0VjFGS2F6RTJSbWhCZWtOQlRsRktaR1F5UTFkRldrcE9lbXBSYjNGSmRYWk5SbUpVWjFKTVNFRWlmUSMwIiwidHlwIjoiSldUIn0.eyJleHAiOjE3MjQ1MzQwNTAsImlzcyI6ImRpZDpqd2s6ZXlKcmRIa2lPaUpQUzFBaUxDSmpjbllpT2lKRlpESTFOVEU1SWl3aWVDSTZJbkY0VjFGS2F6RTJSbWhCZWtOQlRsRktaR1F5UTFkRldrcE9lbXBSYjNGSmRYWk5SbUpVWjFKTVNFRWlmUSIsImp0aSI6InVybjp2Yzp1dWlkOjlkMzdmMzY3LWE4ZDctNDY4Zi05NGYwLTk1NzAxNzBkNzZhNCIsIm5iZiI6MTcyMTk0MjA1MCwidmMiOnsiQGNvbnRleHQiOlsiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiXSwidHlwZSI6WyJWZXJpZmlhYmxlQ3JlZGVudGlhbCJdLCJpc3N1ZXIiOiJkaWQ6andrOmV5SnJkSGtpT2lKUFMxQWlMQ0pqY25ZaU9pSkZaREkxTlRFNUlpd2llQ0k2SW5GNFYxRkthekUyUm1oQmVrTkJUbEZLWkdReVExZEZXa3BPZW1wUmIzRkpkWFpOUm1KVVoxSk1TRUVpZlEiLCJjcmVkZW50aWFsU3ViamVjdCI6eyJpc3N1ZXIiOiJkaWQ6andrOmV5SnJkSGtpT2lKUFMxQWlMQ0pqY25ZaU9pSkZaREkxTlRFNUlpd2llQ0k2SW5GNFYxRkthekUyUm1oQmVrTkJUbEZLWkdReVExZEZXa3BPZW1wUmIzRkpkWFpOUm1KVVoxSk1TRUVpZlEifSwiaWQiOiJ1cm46dmM6dXVpZDo5ZDM3ZjM2Ny1hOGQ3LTQ2OGYtOTRmMC05NTcwMTcwZDc2YTQiLCJpc3N1YW5jZURhdGUiOiIyMDI0LTA3LTI1VDIxOjE0OjEwWiIsImV4cGlyYXRpb25EYXRlIjoiMjAyNC0wOC0yNFQyMToxNDoxMFoiLCJjcmVkZW50aWFsU2NoZW1hIjpbeyJ0eXBlIjoiSnNvblNjaGVtYSIsImlkIjoiaHR0cHM6Ly92Yy5zY2hlbWFzLmhvc3Qva2JjLnNjaGVtYS5qc29uIn1dfX0.VwvrU5Lmv3rn9rzXB0OCxe-MtE5R0876pXsXNLRuQjoqSNB5tBv_12NqrobwA-LkMzFwzdQ5-LWJni6grGdXCQ` 120 | decoded, err := jwt.Decode(vcJwt) 121 | assert.NoError(t, err) 122 | assert.Equal(t, decoded.Header.ALG, "EdDSA") 123 | assert.Equal(t, decoded.Header.KID, "did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJFZDI1NTE5IiwieCI6InF4V1FKazE2RmhBekNBTlFKZGQyQ1dFWkpOempRb3FJdXZNRmJUZ1JMSEEifQ#0") 124 | assert.NotZero(t, decoded.SignerDID) 125 | } 126 | 127 | func Test_Decode_Bad_Header(t *testing.T) { 128 | vcJwt := `kakaHeader.eyJleHAiOjE3MjQ1MzQwNTAsImlzcyI6ImRpZDpqd2s6ZXlKcmRIa2lPaUpQUzFBaUxDSmpjbllpT2lKRlpESTFOVEU1SWl3aWVDSTZJbkY0VjFGS2F6RTJSbWhCZWtOQlRsRktaR1F5UTFkRldrcE9lbXBSYjNGSmRYWk5SbUpVWjFKTVNFRWlmUSIsImp0aSI6InVybjp2Yzp1dWlkOjlkMzdmMzY3LWE4ZDctNDY4Zi05NGYwLTk1NzAxNzBkNzZhNCIsIm5iZiI6MTcyMTk0MjA1MCwidmMiOnsiQGNvbnRleHQiOlsiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiXSwidHlwZSI6WyJWZXJpZmlhYmxlQ3JlZGVudGlhbCJdLCJpc3N1ZXIiOiJkaWQ6andrOmV5SnJkSGtpT2lKUFMxQWlMQ0pqY25ZaU9pSkZaREkxTlRFNUlpd2llQ0k2SW5GNFYxRkthekUyUm1oQmVrTkJUbEZLWkdReVExZEZXa3BPZW1wUmIzRkpkWFpOUm1KVVoxSk1TRUVpZlEiLCJjcmVkZW50aWFsU3ViamVjdCI6eyJpc3N1ZXIiOiJkaWQ6andrOmV5SnJkSGtpT2lKUFMxQWlMQ0pqY25ZaU9pSkZaREkxTlRFNUlpd2llQ0k2SW5GNFYxRkthekUyUm1oQmVrTkJUbEZLWkdReVExZEZXa3BPZW1wUmIzRkpkWFpOUm1KVVoxSk1TRUVpZlEifSwiaWQiOiJ1cm46dmM6dXVpZDo5ZDM3ZjM2Ny1hOGQ3LTQ2OGYtOTRmMC05NTcwMTcwZDc2YTQiLCJpc3N1YW5jZURhdGUiOiIyMDI0LTA3LTI1VDIxOjE0OjEwWiIsImV4cGlyYXRpb25EYXRlIjoiMjAyNC0wOC0yNFQyMToxNDoxMFoiLCJjcmVkZW50aWFsU2NoZW1hIjpbeyJ0eXBlIjoiSnNvblNjaGVtYSIsImlkIjoiaHR0cHM6Ly92Yy5zY2hlbWFzLmhvc3Qva2JjLnNjaGVtYS5qc29uIn1dfX0.VwvrU5Lmv3rn9rzXB0OCxe-MtE5R0876pXsXNLRuQjoqSNB5tBv_12NqrobwA-LkMzFwzdQ5-LWJni6grGdXCQ` 129 | _, err := jwt.Decode(vcJwt) 130 | assert.Error(t, err) 131 | } 132 | 133 | func Test_Decode_Bad_Signature(t *testing.T) { 134 | vcJwt := `eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpqd2s6ZXlKcmRIa2lPaUpQUzFBaUxDSmpjbllpT2lKRlpESTFOVEU1SWl3aWVDSTZJbkY0VjFGS2F6RTJSbWhCZWtOQlRsRktaR1F5UTFkRldrcE9lbXBSYjNGSmRYWk5SbUpVWjFKTVNFRWlmUSMwIiwidHlwIjoiSldUIn0.eyJleHAiOjE3MjQ1MzQwNTAsImlzcyI6ImRpZDpqd2s6ZXlKcmRIa2lPaUpQUzFBaUxDSmpjbllpT2lKRlpESTFOVEU1SWl3aWVDSTZJbkY0VjFGS2F6RTJSbWhCZWtOQlRsRktaR1F5UTFkRldrcE9lbXBSYjNGSmRYWk5SbUpVWjFKTVNFRWlmUSIsImp0aSI6InVybjp2Yzp1dWlkOjlkMzdmMzY3LWE4ZDctNDY4Zi05NGYwLTk1NzAxNzBkNzZhNCIsIm5iZiI6MTcyMTk0MjA1MCwidmMiOnsiQGNvbnRleHQiOlsiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiXSwidHlwZSI6WyJWZXJpZmlhYmxlQ3JlZGVudGlhbCJdLCJpc3N1ZXIiOiJkaWQ6andrOmV5SnJkSGtpT2lKUFMxQWlMQ0pqY25ZaU9pSkZaREkxTlRFNUlpd2llQ0k2SW5GNFYxRkthekUyUm1oQmVrTkJUbEZLWkdReVExZEZXa3BPZW1wUmIzRkpkWFpOUm1KVVoxSk1TRUVpZlEiLCJjcmVkZW50aWFsU3ViamVjdCI6eyJpc3N1ZXIiOiJkaWQ6andrOmV5SnJkSGtpT2lKUFMxQWlMQ0pqY25ZaU9pSkZaREkxTlRFNUlpd2llQ0k2SW5GNFYxRkthekUyUm1oQmVrTkJUbEZLWkdReVExZEZXa3BPZW1wUmIzRkpkWFpOUm1KVVoxSk1TRUVpZlEifSwiaWQiOiJ1cm46dmM6dXVpZDo5ZDM3ZjM2Ny1hOGQ3LTQ2OGYtOTRmMC05NTcwMTcwZDc2YTQiLCJpc3N1YW5jZURhdGUiOiIyMDI0LTA3LTI1VDIxOjE0OjEwWiIsImV4cGlyYXRpb25EYXRlIjoiMjAyNC0wOC0yNFQyMToxNDoxMFoiLCJjcmVkZW50aWFsU2NoZW1hIjpbeyJ0eXBlIjoiSnNvblNjaGVtYSIsImlkIjoiaHR0cHM6Ly92Yy5zY2hlbWFzLmhvc3Qva2JjLnNjaGVtYS5qc29uIn1dfX0.kakaSignature` 135 | _, err := jwt.Decode(vcJwt) 136 | assert.Error(t, err) 137 | } 138 | 139 | func Test_Decode_HeaderKID_InvalidDID(t *testing.T) { 140 | vcJwt := `eyJhbGciOiJFZERTQSIsImtpZCI6Imtha2EiLCJ0eXAiOiJKV1QifQ.eyJleHAiOjE3MjQ1MzQwNTAsImlzcyI6ImRpZDpqd2s6ZXlKcmRIa2lPaUpQUzFBaUxDSmpjbllpT2lKRlpESTFOVEU1SWl3aWVDSTZJbkY0VjFGS2F6RTJSbWhCZWtOQlRsRktaR1F5UTFkRldrcE9lbXBSYjNGSmRYWk5SbUpVWjFKTVNFRWlmUSIsImp0aSI6InVybjp2Yzp1dWlkOjlkMzdmMzY3LWE4ZDctNDY4Zi05NGYwLTk1NzAxNzBkNzZhNCIsIm5iZiI6MTcyMTk0MjA1MCwidmMiOnsiQGNvbnRleHQiOlsiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiXSwidHlwZSI6WyJWZXJpZmlhYmxlQ3JlZGVudGlhbCJdLCJpc3N1ZXIiOiJkaWQ6andrOmV5SnJkSGtpT2lKUFMxQWlMQ0pqY25ZaU9pSkZaREkxTlRFNUlpd2llQ0k2SW5GNFYxRkthekUyUm1oQmVrTkJUbEZLWkdReVExZEZXa3BPZW1wUmIzRkpkWFpOUm1KVVoxSk1TRUVpZlEiLCJjcmVkZW50aWFsU3ViamVjdCI6eyJpc3N1ZXIiOiJkaWQ6andrOmV5SnJkSGtpT2lKUFMxQWlMQ0pqY25ZaU9pSkZaREkxTlRFNUlpd2llQ0k2SW5GNFYxRkthekUyUm1oQmVrTkJUbEZLWkdReVExZEZXa3BPZW1wUmIzRkpkWFpOUm1KVVoxSk1TRUVpZlEifSwiaWQiOiJ1cm46dmM6dXVpZDo5ZDM3ZjM2Ny1hOGQ3LTQ2OGYtOTRmMC05NTcwMTcwZDc2YTQiLCJpc3N1YW5jZURhdGUiOiIyMDI0LTA3LTI1VDIxOjE0OjEwWiIsImV4cGlyYXRpb25EYXRlIjoiMjAyNC0wOC0yNFQyMToxNDoxMFoiLCJjcmVkZW50aWFsU2NoZW1hIjpbeyJ0eXBlIjoiSnNvblNjaGVtYSIsImlkIjoiaHR0cHM6Ly92Yy5zY2hlbWFzLmhvc3Qva2JjLnNjaGVtYS5qc29uIn1dfX0.VwvrU5Lmv3rn9rzXB0OCxe-MtE5R0876pXsXNLRuQjoqSNB5tBv_12NqrobwA-LkMzFwzdQ5-LWJni6grGdXCQ` 141 | _, err := jwt.Decode(vcJwt) 142 | assert.Error(t, err) 143 | } 144 | -------------------------------------------------------------------------------- /pexv2/pd.go: -------------------------------------------------------------------------------- 1 | package pexv2 2 | 3 | import ( 4 | "encoding/base64" 5 | "encoding/json" 6 | "fmt" 7 | "github.com/PaesslerAG/jsonpath" 8 | "github.com/decentralized-identity/web5-go/vc" 9 | "github.com/santhosh-tekuri/jsonschema/v5" 10 | "math/rand" 11 | "strconv" 12 | ) 13 | 14 | // PresentationDefinition represents a DIF Presentation Definition defined [here]. 15 | // Presentation Definitions are objects that articulate what proofs a Verifier requires 16 | // 17 | // [here]: https://identity.foundation/presentation-exchange/#presentation-definition 18 | type PresentationDefinition struct { 19 | ID string `json:"id"` 20 | Name string `json:"name,omitempty"` 21 | Purpose string `json:"purpose,omitempty"` 22 | InputDescriptors []InputDescriptor `json:"input_descriptors"` 23 | } 24 | 25 | // InputDescriptor represents a DIF Input Descriptor defined [here]. 26 | // Input Descriptors are used to describe the information a Verifier requires of a Holder. 27 | // 28 | // [here]: https://identity.foundation/presentation-exchange/#input-descriptor 29 | type InputDescriptor struct { 30 | ID string `json:"id"` 31 | Name string `json:"name,omitempty"` 32 | Purpose string `json:"purpose,omitempty"` 33 | Constraints Constraints `json:"constraints"` 34 | } 35 | 36 | type tokenizedField struct { 37 | name string 38 | path string 39 | } 40 | 41 | // SelectCredentials selects vcJWTs based on the constraints defined in the input descriptor 42 | func (ind InputDescriptor) SelectCredentials(vcJWTs []string) ([]string, error) { 43 | jsonSchema := JSONSchema{ 44 | Schema: "http://json-schema.org/draft-07/schema#", 45 | Type: "object", 46 | Properties: make(map[string]Filter, len(ind.Constraints.Fields)), 47 | Required: make([]string, 0, len(ind.Constraints.Fields)), 48 | } 49 | 50 | // Each Field can have multiple Paths. Add a 'tokenizedField' for each Path, and add the Filter to the JSON Schema 51 | tokenizedFields := make([]tokenizedField, 0, len(ind.Constraints.Fields)) 52 | for _, field := range ind.Constraints.Fields { 53 | name := strconv.FormatInt(rand.Int63(), 10) //nolint:gosec 54 | for _, path := range field.Path { 55 | tf := tokenizedField{name: name, path: path} 56 | tokenizedFields = append(tokenizedFields, tf) 57 | } 58 | 59 | if field.Filter != nil { 60 | jsonSchema.AddProperty(name, *field.Filter, true) 61 | } 62 | } 63 | 64 | sch, err := json.Marshal(jsonSchema) 65 | if err != nil { 66 | return nil, fmt.Errorf("error marshalling schema: %w", err) 67 | } 68 | 69 | schema, err := jsonschema.CompileString(ind.ID, string(sch)) 70 | if err != nil { 71 | return nil, fmt.Errorf("error compiling schema: %w", err) 72 | } 73 | 74 | matched := make([]string, 0, len(vcJWTs)) 75 | 76 | for _, vcJWT := range vcJWTs { 77 | tokensFound := make(map[string]bool, len(tokenizedFields)) 78 | 79 | decoded, err := vc.Decode[vc.Claims](vcJWT) 80 | if err != nil { 81 | continue 82 | } 83 | 84 | var jwtPayload map[string]any 85 | payload, err := base64.RawURLEncoding.DecodeString(decoded.JWT.Parts[1]) 86 | if err != nil { 87 | continue 88 | } 89 | 90 | if err := json.Unmarshal(payload, &jwtPayload); err != nil { 91 | continue 92 | } 93 | 94 | selectionCandidate := make(map[string]any) 95 | for _, tf := range tokenizedFields { 96 | if ok := tokensFound[tf.name]; ok { 97 | continue 98 | } 99 | 100 | value, err := jsonpath.Get(tf.path, jwtPayload) 101 | if err != nil { 102 | continue 103 | } 104 | 105 | if value != nil { 106 | selectionCandidate[tf.name] = value 107 | tokensFound[tf.name] = true 108 | } 109 | } 110 | 111 | if len(selectionCandidate) != len(ind.Constraints.Fields) { 112 | continue 113 | } 114 | 115 | if err := schema.Validate(selectionCandidate); err != nil { 116 | continue 117 | } 118 | 119 | matched = append(matched, vcJWT) 120 | } 121 | 122 | return matched, nil 123 | } 124 | 125 | // Constraints contains the requirements for a given Input Descriptor. 126 | type Constraints struct { 127 | Fields []Field `json:"fields,omitempty"` 128 | } 129 | 130 | // Field contains the requirements for a given field within a proof 131 | type Field struct { 132 | ID string `json:"id,omitempty"` 133 | Name string `json:"name,omitempty"` 134 | Path []string `json:"path,omitempty"` 135 | Purpose string `json:"purpose,omitempty"` 136 | Filter *Filter `json:"filter,omitempty"` 137 | Optional bool `json:"optional,omitempty"` 138 | Predicate *Optionality `json:"predicate,omitempty"` 139 | } 140 | 141 | // Optionality is a type alias for the possible values of the predicate field 142 | type Optionality string 143 | 144 | // Constants for Optionality values 145 | const ( 146 | Required Optionality = "required" 147 | Preferred Optionality = "preferred" 148 | ) 149 | 150 | // Filter is a JSON Schema that is applied against the value of a field. 151 | type Filter struct { 152 | Type string `json:"type,omitempty"` 153 | Pattern string `json:"pattern,omitempty"` 154 | Const string `json:"const,omitempty"` 155 | Contains *Filter `json:"contains,omitempty"` 156 | } 157 | 158 | // JSONSchema represents a minimal JSON Schema 159 | type JSONSchema struct { 160 | Schema string `json:"$schema"` 161 | Type string `json:"type"` 162 | Properties map[string]Filter `json:"properties"` 163 | Required []string `json:"required"` 164 | } 165 | 166 | // AddProperty adds the provided Filter with the provided name to the JsonSchema 167 | func (j *JSONSchema) AddProperty(name string, value Filter, required bool) { 168 | j.Properties[name] = value 169 | 170 | if required { 171 | j.Required = append(j.Required, name) 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /pexv2/pexv2.go: -------------------------------------------------------------------------------- 1 | package pexv2 2 | 3 | import "fmt" 4 | 5 | // SelectCredentials selects vcJWTs based on the constraints defined in the presentation definition 6 | func SelectCredentials(vcJWTs []string, pd PresentationDefinition) ([]string, error) { 7 | matchSet := make(map[string]bool, len(vcJWTs)) 8 | matched := make([]string, 0, len(matchSet)) 9 | 10 | for _, inputDescriptor := range pd.InputDescriptors { 11 | matches, err := inputDescriptor.SelectCredentials(vcJWTs) 12 | if err != nil { 13 | return nil, fmt.Errorf("failed to satisfy input descriptor constraints %s: %w", inputDescriptor.ID, err) 14 | } 15 | 16 | if len(matches) == 0 { 17 | return matched, nil 18 | } 19 | 20 | // Add all matches to the match set 21 | for _, vcJWT := range matches { 22 | matchSet[vcJWT] = true 23 | } 24 | } 25 | 26 | // add all unique matches to the matched slice 27 | for k := range matchSet { 28 | matched = append(matched, k) 29 | } 30 | 31 | return matched, nil 32 | } 33 | -------------------------------------------------------------------------------- /pexv2/pexv2_test.go: -------------------------------------------------------------------------------- 1 | package pexv2_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/alecthomas/assert/v2" 8 | "github.com/decentralized-identity/web5-go" 9 | "github.com/decentralized-identity/web5-go/pexv2" 10 | testify "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | type PresentationInput struct { 14 | PresentationDefinition pexv2.PresentationDefinition `json:"presentationDefinition"` 15 | CredentialJwts []string `json:"credentialJwts"` 16 | } 17 | 18 | type PresentationOutput struct { 19 | SelectedCredentials []string `json:"selectedCredentials"` 20 | } 21 | 22 | func TestSelectCredentials(t *testing.T) { 23 | testVectors, err := web5.LoadTestVectors[PresentationInput, PresentationOutput]("../web5-spec/test-vectors/presentation_exchange/select_credentials.json") 24 | assert.NoError(t, err) 25 | 26 | for _, vector := range testVectors.Vectors { 27 | t.Run(vector.Description, func(t *testing.T) { 28 | fmt.Println("Running test vector: ", vector.Description) 29 | 30 | vcJwts, err := pexv2.SelectCredentials(vector.Input.CredentialJwts, vector.Input.PresentationDefinition) 31 | 32 | assert.NoError(t, err) 33 | testify.ElementsMatch(t, vector.Output.SelectedCredentials, vcJwts) 34 | }) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /scripts/web5: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | basedir="$(dirname "$0")/.." 4 | name="$(basename "$0")" 5 | dest="${basedir}/build/devel" 6 | mkdir -p "$dest" 7 | (cd "${basedir}" && ./bin/go build -ldflags="-s -w -buildid=" -o "$dest/${name}" "./cmd/${name}") && exec "$dest/${name}" "$@" 8 | -------------------------------------------------------------------------------- /testvectors.go: -------------------------------------------------------------------------------- 1 | package web5 2 | 3 | import ( 4 | "encoding/json" 5 | "os" 6 | ) 7 | 8 | // TestVectors are JSON files which are tested against to ensure interop with the specification 9 | type TestVectors[T, U any] struct { 10 | Description string `json:"description"` 11 | Vectors []TestVector[T, U] `json:"vectors"` 12 | } 13 | 14 | // TestVector is an individual test vector case 15 | type TestVector[I, O any] struct { 16 | Description string `json:"description"` 17 | Input I `json:"input"` 18 | Output O `json:"output"` 19 | Errors bool `json:"errors"` 20 | } 21 | 22 | // LoadTestVectors is for reading the vector at the given path 23 | func LoadTestVectors[I, O any](path string) (TestVectors[I, O], error) { 24 | data, err := os.ReadFile(path) 25 | if err != nil { 26 | return TestVectors[I, O]{}, err 27 | } 28 | 29 | var testVectors TestVectors[I, O] 30 | err = json.Unmarshal(data, &testVectors) 31 | if err != nil { 32 | return TestVectors[I, O]{}, err 33 | } 34 | 35 | return testVectors, nil 36 | } 37 | -------------------------------------------------------------------------------- /vc/examples_test.go: -------------------------------------------------------------------------------- 1 | package vc_test 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/decentralized-identity/web5-go/dids/didjwk" 9 | "github.com/decentralized-identity/web5-go/vc" 10 | ) 11 | 12 | // Demonstrates how to create, sign, and verify a Verifiable Credential using the vc package. 13 | func Example() { 14 | // create sample issuer and subject DIDs 15 | issuer, err := didjwk.Create() 16 | if err != nil { 17 | panic(err) 18 | } 19 | 20 | subject, err := didjwk.Create() 21 | if err != nil { 22 | panic(err) 23 | } 24 | 25 | // creation 26 | claims := vc.Claims{"id": subject.URI, "name": "Randy McRando"} 27 | cred := vc.Create(claims) 28 | 29 | // signing 30 | vcJWT, err := cred.Sign(issuer) 31 | if err != nil { 32 | panic(err) 33 | } 34 | 35 | // verification 36 | decoded, err := vc.Verify[vc.Claims](vcJWT) 37 | if err != nil { 38 | panic(err) 39 | } 40 | 41 | fmt.Println(decoded.VC.CredentialSubject["name"]) 42 | // Output: Randy McRando 43 | } 44 | 45 | type KnownCustomerClaims struct { 46 | ID string `json:"id"` 47 | Name string `json:"name"` 48 | } 49 | 50 | func (c KnownCustomerClaims) GetID() string { 51 | return c.ID 52 | } 53 | 54 | func (c *KnownCustomerClaims) SetID(id string) { 55 | c.ID = id 56 | } 57 | 58 | // Demonstrates how to use a strongly typed credential subject 59 | func Example_stronglyTyped() { 60 | issuer, err := didjwk.Create() 61 | if err != nil { 62 | panic(err) 63 | } 64 | 65 | subject, err := didjwk.Create() 66 | if err != nil { 67 | panic(err) 68 | } 69 | 70 | claims := KnownCustomerClaims{ID: subject.URI, Name: "Randy McRando"} 71 | cred := vc.Create(&claims) 72 | 73 | vcJWT, err := cred.Sign(issuer) 74 | if err != nil { 75 | panic(err) 76 | } 77 | 78 | decoded, err := vc.Verify[*KnownCustomerClaims](vcJWT) 79 | if err != nil { 80 | panic(err) 81 | } 82 | 83 | fmt.Println(decoded.VC.CredentialSubject.Name) 84 | // Output: Randy McRando 85 | } 86 | 87 | // Demonstrates how to use a mix of strongly typed and untyped credential subjects with the vc package. 88 | func Example_mixed() { 89 | issuer, err := didjwk.Create() 90 | if err != nil { 91 | panic(err) 92 | } 93 | 94 | subject, err := didjwk.Create() 95 | if err != nil { 96 | panic(err) 97 | } 98 | 99 | claims := KnownCustomerClaims{ID: subject.URI, Name: "Randy McRando"} 100 | cred := vc.Create(&claims) 101 | 102 | vcJWT, err := cred.Sign(issuer) 103 | if err != nil { 104 | panic(err) 105 | } 106 | 107 | decoded, err := vc.Verify[vc.Claims](vcJWT) 108 | if err != nil { 109 | panic(err) 110 | } 111 | 112 | fmt.Println(decoded.VC.CredentialSubject["name"]) 113 | // Output: Randy McRando 114 | } 115 | 116 | // Demonstrates how to create a Verifiable Credential 117 | func ExampleCreate() { 118 | claims := vc.Claims{"name": "Randy McRando"} 119 | cred := vc.Create(claims) 120 | 121 | bytes, err := json.MarshalIndent(cred, "", " ") 122 | if err != nil { 123 | panic(err) 124 | } 125 | 126 | fmt.Println(string(bytes)) 127 | } 128 | 129 | // Demonstrates how to create a Verifiable Credential with options 130 | func ExampleCreate_options() { 131 | claims := vc.Claims{"id": "1234"} 132 | issuanceDate := time.Now().UTC().Add(10 * time.Hour) 133 | expirationDate := issuanceDate.Add(30 * time.Hour) 134 | 135 | cred := vc.Create( 136 | claims, 137 | vc.ID("hehecustomid"), 138 | vc.Contexts("https://nocontextisbestcontext.gov"), 139 | vc.Types("StreetCredential"), 140 | vc.IssuanceDate(issuanceDate), 141 | vc.ExpirationDate(expirationDate), 142 | ) 143 | 144 | bytes, err := json.MarshalIndent(cred, "", " ") 145 | if err != nil { 146 | panic(err) 147 | } 148 | 149 | fmt.Println(string(bytes)) 150 | } 151 | -------------------------------------------------------------------------------- /vc/vc_test.go: -------------------------------------------------------------------------------- 1 | package vc_test 2 | 3 | import ( 4 | "slices" 5 | "testing" 6 | "time" 7 | 8 | "github.com/alecthomas/assert/v2" 9 | "github.com/decentralized-identity/web5-go/dids/didjwk" 10 | "github.com/decentralized-identity/web5-go/vc" 11 | ) 12 | 13 | func TestCreate_Defaults(t *testing.T) { 14 | cred := vc.Create(vc.Claims{"id": "1234"}) 15 | 16 | assert.Equal(t, 1, len(cred.Context)) 17 | assert.Equal(t, vc.BaseContext, cred.Context[0]) 18 | 19 | assert.Equal(t, 1, len(cred.Type)) 20 | assert.Equal(t, vc.BaseType, cred.Type[0]) 21 | 22 | assert.Contains(t, cred.ID, "urn:vc:uuid:") 23 | 24 | assert.NotZero(t, cred.IssuanceDate) 25 | 26 | _, err := time.Parse(time.RFC3339, cred.IssuanceDate) 27 | assert.NoError(t, err) 28 | 29 | assert.Equal(t, "1234", cred.CredentialSubject["id"]) 30 | } 31 | 32 | func TestCreate_Options(t *testing.T) { 33 | claims := vc.Claims{"id": "1234"} 34 | issuanceDate := time.Now().UTC().Add(10 * time.Hour) 35 | expirationDate := issuanceDate.Add(30 * time.Hour) 36 | 37 | cred := vc.Create( 38 | claims, 39 | vc.ID("hehecustomid"), 40 | vc.Contexts("https://nocontextisbestcontext.gov"), 41 | vc.Types("StreetCredential"), 42 | vc.IssuanceDate(issuanceDate), 43 | vc.ExpirationDate(expirationDate), 44 | vc.Schemas("https://example.org/examples/degree.json"), 45 | vc.Evidences(vc.Evidence{ 46 | ID: "evidenceID", 47 | Type: "Insufficient", 48 | AdditionalFields: map[string]interface{}{ 49 | "kind": "circumstantial", 50 | "checks": []string{"motive", "cell_tower_logs"}, 51 | }, 52 | }), 53 | ) 54 | 55 | assert.Equal(t, 2, len(cred.Context)) 56 | assert.True(t, slices.Contains(cred.Context, "https://nocontextisbestcontext.gov")) 57 | assert.True(t, slices.Contains(cred.Context, vc.BaseContext)) 58 | 59 | assert.Equal(t, 2, len(cred.Type)) 60 | assert.True(t, slices.Contains(cred.Type, "StreetCredential")) 61 | assert.True(t, slices.Contains(cred.Type, vc.BaseType)) 62 | 63 | assert.Equal(t, "hehecustomid", cred.ID) 64 | 65 | assert.NotZero(t, cred.ExpirationDate) 66 | 67 | assert.Equal(t, 1, len(cred.CredentialSchema)) 68 | assert.Equal(t, "https://example.org/examples/degree.json", cred.CredentialSchema[0].ID) 69 | assert.Equal(t, "JsonSchema", cred.CredentialSchema[0].Type) 70 | 71 | assert.Equal(t, 1, len(cred.Evidence)) 72 | assert.Equal(t, "evidenceID", cred.Evidence[0].ID) 73 | assert.Equal(t, "Insufficient", cred.Evidence[0].Type) 74 | assert.Equal(t, "circumstantial", cred.Evidence[0].AdditionalFields["kind"]) 75 | assert.Equal(t, []string{"motive", "cell_tower_logs"}, cred.Evidence[0].AdditionalFields["checks"].([]string)) // nolint:forcetypeassert 76 | } 77 | 78 | func TestSign(t *testing.T) { 79 | issuer, err := didjwk.Create() 80 | assert.NoError(t, err) 81 | 82 | subject, err := didjwk.Create() 83 | assert.NoError(t, err) 84 | 85 | claims := vc.Claims{"id": subject.URI, "name": "Randy McRando"} 86 | cred := vc.Create(claims) 87 | 88 | vcJWT, err := cred.Sign(issuer) 89 | assert.NoError(t, err) 90 | assert.NotZero(t, vcJWT) 91 | 92 | // TODO: make test more reliable by not depending on another function in this package (Moe - 2024-02-25) 93 | decoded, err := vc.Verify[vc.Claims](vcJWT) 94 | 95 | assert.NoError(t, err) 96 | assert.NotZero(t, decoded) 97 | } 98 | -------------------------------------------------------------------------------- /vc/vcjwt.go: -------------------------------------------------------------------------------- 1 | package vc 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "slices" 8 | "time" 9 | 10 | "github.com/decentralized-identity/web5-go/jwt" 11 | ) 12 | 13 | // Verify decodes and verifies the vc-jwt. It checks for the presence of required fields and verifies the jwt. 14 | // It returns the decoded vc-jwt and the verification result. 15 | func Verify[T CredentialSubject](vcJWT string) (DecodedVCJWT[T], error) { 16 | decoded, err := Decode[T](vcJWT) 17 | if err != nil { 18 | return decoded, err 19 | } 20 | 21 | return decoded, decoded.Verify() 22 | } 23 | 24 | // Decode decodes a vc-jwt as per the [spec] and returns [DecodedVCJWT]. 25 | // 26 | // # Note 27 | // 28 | // This function uses certain fields from the jwt claims to eagrly populate the vc model as described 29 | // in the encoding section of the spec. The jwt fields will clobber any values that exist in the vc model. 30 | // While the jwt claims should match the counterpart values in the vc model, it's possible that they don't 31 | // but there would be no way to know if they don't match given that they're overwritten. 32 | // 33 | // [spec]: https://www.w3.org/TR/vc-data-model/#json-web-token 34 | func Decode[T CredentialSubject](vcJWT string) (DecodedVCJWT[T], error) { 35 | decoded, err := jwt.Decode(vcJWT) 36 | if err != nil { 37 | return DecodedVCJWT[T]{}, fmt.Errorf("failed to decode vc-jwt: %w", err) 38 | } 39 | 40 | if decoded.Claims.Misc == nil { 41 | return DecodedVCJWT[T]{}, errors.New("vc-jwt missing vc claim") 42 | } 43 | 44 | if _, ok := decoded.Claims.Misc["vc"]; ok == false { 45 | return DecodedVCJWT[T]{}, errors.New("vc-jwt missing vc claim") 46 | } 47 | 48 | bytes, err := json.Marshal(decoded.Claims.Misc["vc"]) 49 | if err != nil { 50 | return DecodedVCJWT[T]{}, fmt.Errorf("failed to decode vc claim: %w", err) 51 | } 52 | 53 | var vc DataModel[T] 54 | if err := json.Unmarshal(bytes, &vc); err != nil { 55 | return DecodedVCJWT[T]{}, fmt.Errorf("failed to decode vc claim: %w", err) 56 | } 57 | 58 | if vc.Type == nil { 59 | return DecodedVCJWT[T]{}, errors.New("vc-jwt missing vc type") 60 | } 61 | 62 | // the following conditionals are included to conform with the jwt decoding section 63 | // of the specification defined here: https://www.w3.org/TR/vc-data-model/#jwt-decoding 64 | if decoded.Claims.Issuer != "" { 65 | vc.Issuer = decoded.Claims.Issuer 66 | } 67 | 68 | if decoded.Claims.JTI != "" { 69 | vc.ID = decoded.Claims.JTI 70 | } 71 | 72 | if decoded.Claims.Subject != "" { 73 | vc.CredentialSubject.SetID(decoded.Claims.Subject) 74 | } 75 | 76 | if decoded.Claims.Expiration != 0 { 77 | vc.ExpirationDate = time.Unix(decoded.Claims.Expiration, 0).UTC().Format(time.RFC3339) 78 | } 79 | 80 | if decoded.Claims.NotBefore != 0 { 81 | vc.IssuanceDate = time.Unix(decoded.Claims.NotBefore, 0).UTC().Format(time.RFC3339) 82 | } 83 | 84 | return DecodedVCJWT[T]{ 85 | JWT: decoded, 86 | VC: vc, 87 | }, nil 88 | 89 | } 90 | 91 | // DecodedVCJWT represents a decoded vc-jwt. It contains the decoded jwt and decoded vc data model 92 | type DecodedVCJWT[T CredentialSubject] struct { 93 | JWT jwt.Decoded 94 | VC DataModel[T] 95 | } 96 | 97 | // Verify verifies the decoded vc-jwt. It checks for the presence of required fields and verifies the jwt. 98 | func (vcjwt DecodedVCJWT[T]) Verify() error { 99 | if vcjwt.JWT.Header.TYP != "JWT" { 100 | return errors.New("invalid typ") 101 | } 102 | 103 | if vcjwt.VC.Issuer == "" { 104 | return errors.New("missing issuer") 105 | } 106 | 107 | if vcjwt.VC.ID == "" { 108 | return errors.New("missing id") 109 | } 110 | 111 | if vcjwt.VC.IssuanceDate == "" { 112 | return errors.New("missing issuance date") 113 | } 114 | 115 | issuanceDate, err := time.Parse(time.RFC3339, vcjwt.VC.IssuanceDate) 116 | if err != nil { 117 | return fmt.Errorf("failed to parse issuance date: %w", err) 118 | } 119 | 120 | if time.Now().UTC().Before(issuanceDate.UTC()) { 121 | return fmt.Errorf("vc cannot be used before %s", vcjwt.VC.IssuanceDate) 122 | } 123 | 124 | if vcjwt.VC.ExpirationDate != "" { 125 | exp, err := time.Parse(time.RFC3339, vcjwt.VC.ExpirationDate) 126 | if err != nil { 127 | return fmt.Errorf("failed to parse expiration date: %w", err) 128 | } 129 | 130 | if time.Now().UTC().After(exp.UTC()) { 131 | return fmt.Errorf("vc expired on %s", vcjwt.VC.ExpirationDate) 132 | } 133 | } 134 | 135 | if vcjwt.VC.Type == nil || len(vcjwt.VC.Type) == 0 { 136 | return errors.New("missing type") 137 | } 138 | 139 | if slices.Contains(vcjwt.VC.Type, BaseType) == false { 140 | return fmt.Errorf("missing base type: %s", BaseType) 141 | } 142 | 143 | if vcjwt.VC.Context == nil || len(vcjwt.VC.Context) == 0 { 144 | return errors.New("missing @context") 145 | } 146 | 147 | if slices.Contains(vcjwt.VC.Context, BaseContext) == false { 148 | return fmt.Errorf("missing base @context: %s", BaseContext) 149 | } 150 | 151 | err = vcjwt.JWT.Verify() 152 | if err != nil { 153 | return fmt.Errorf("integrity check mismatch: %w", err) 154 | } 155 | 156 | return nil 157 | } 158 | --------------------------------------------------------------------------------