Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement did generator #3

Merged
merged 19 commits into from
Jun 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 4 additions & 7 deletions .github/workflows/build-python-app.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ permissions:
pull-requests: write

jobs:
build:
build-python-app:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
Expand All @@ -35,8 +35,8 @@ jobs:

- name: Test with pytest
run: |
python -m pytest --cov --cov-report=xml tests
coverage report -m
python -m pytest --cov --cov-report=xml
coverage report
shell: bash

- name: Code Coverage Report
Expand All @@ -54,10 +54,7 @@ jobs:

- name: Add Coverage PR Comment
uses: marocchino/sticky-pull-request-comment@v2
if: github.event_name == 'pull_request'
if: ${{ !cancelled() && github.event_name == 'pull_request' }}
with:
recreate: true
path: code-coverage-results.md

- name: Write to Job Summary
run: cat code-coverage-results.md >> $GITHUB_STEP_SUMMARY
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.idea
.coverage
coverage.xml
config.yml
config.yaml
node_modules
venv/
__pycache__/

4 changes: 4 additions & 0 deletions .lintstagedrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"*": "prettier --ignore-unknown --write",
"*.md": "markdownlint-cli2-fix"
}
55 changes: 55 additions & 0 deletions .markdownlint-cli2.jsonc
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
{
"config": {
"default": true,
"MD013": false,
"MD033": true,
"MD024": false,
"search-replace": {
"rules": [
{
"name": "curly-double-quotes",
"message": "Don't use curly double quotes",
"searchPattern": "/“|”/g",
"replace": "\"",
"skipCode": true
},
{
"name": "curly-single-quotes",
"message": "Don't use curly single quotes",
"searchPattern": "/‘|’/g",
"replace": "'",
"skipCode": true
},
{
"name": "m-dash",
"message": "Don't use '--'. Use m-dash — instead",
"search": " -- ",
"replace": " — ",
"skipCode": true
},
{
"name": "relative-link-path",
"message": "Don't use relative paths",
"search": "](..",
"skipCode": true
},
{
"name": "trailing-spaces",
"message": "Avoid trailing spaces",
"searchPattern": "/ +$/gm",
"replace": "",
"skipCode": false
},
{
"name": "double-spaces",
"message": "Avoid double spaces",
"searchPattern": "/([^\\s>]) ([^\\s|])/g",
"replace": "$1 $2",
"skipCode": true
}
]
}
},
"customRules": ["markdownlint-rule-search-replace"],
"ignores": ["node_modules", ".github"]
}
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
v18
6 changes: 6 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# This .prettierignore file uses .gitignore syntax
# see https://prettier.io/docs/en/ignore.html#ignoring-files-prettierignore

.github
.husky
node_modules
3 changes: 3 additions & 0 deletions .prettierrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"bracketSameLine": true
}
108 changes: 107 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,108 @@
# scs-did-creator
Mamage the creation of did:web for SCS conformant clouds

Tools for managing creation of [DID](https://www.w3.org/TR/did-core/) documents for SCS conformant clouds. Currently, scs-did-creator supports [did:web](https://w3c-ccg.github.io/did-method-web/) only.

## Quick Start

### Installation

We recommend to run scs-did-creator within a [python virtual environment](https://docs.python.org/3/library/venv.html)

Install dependencies

```shell
pip install -r requirements.txt
```

### Pre-requisites

Bases on [DID specification](https://www.w3.org/TR/did-core/#dfn-did-documents): "...DID documents contain information associated with a DID. They typically express verification methods, such as cryptographic public keys, and services relevant to interactions with the DID subject..."

In context of Gaia-X, DID document contains at least one verification method to verify authorship of Gaia-X Credentials. scs-did-creator supports [RSA keys](https://en.wikipedia.org/wiki/RSA_(cryptosystem)) and [EC keys](https://en.wikipedia.org/wiki/Elliptic-curve_cryptography) as verification methods, only. If not already exist, create a public-private key pair with [OpenSSL](https://developers.yubico.com/PIV/Guides/Generating_keys_using_OpenSSL.html)

### Configure scs-did-creator

Mandatory content for DID document such as issuer and verification methods are taken from `config.yaml`.

```yaml
issuer: "did:web:example.com"
verification-methods:
keys:
- "/example1.pem.pub"
- "/example2.pem.pub"
x509s:
- "/cert1.pem"
- "https://www.example.com/cert2.pem"
```

Attribute `issuer` defines issuer of DID document, which is the DID itself. scs-did-creator does support did:web only.
Attribute `verification-methods` refers to a list of public key files or x509 certificates, set as verification method in generated DID document. A public key file must be set as absolute path, an x509 certificate can be an URL or an absolute file path.
Currently, all verification methods are set as `assertionMethod`, i.e. used to verify [Verifiable Credentials](https://www.w3.org/TR/vc-data-model-2.0/) issued by `issuer`.

### Run scs-did-creator

Run scs-did-creator with configuration file, implicitly.

```shell
python3 -m creator.cli
```

Run scs-did-creator with own configuration file

```shell
python3 -m creator.cli --config=my-config.template.yaml
```

scs-did-creator will print generated DID document on screen. There is also an option to write it to an output file instead of stdout.

```shell
python3 -m creator.cli --output-file=my-did-document.json
```

## Developers Guide

scs-did-creator uses [jinja templates](https://jinja.palletsprojects.com/en/3.1.x/) to generate DID documents.

### Running unit tests

First, install the test dependencies in addition to the main dependencies into your virtualenv as described above under "Quick Start Guide":

```shell
pip install -r test-requirements.txt
```

Then, tests can be run with:

```shell
python3 -m pytest
```

To run tests with code coverage, use

```shell
python -m pytest --cov
```

### Updating the dependency pins

We pin dependencies with `pip-compile` from [pip-tools](https://pypi.org/project/pip-tools/), which can be installed with:

```shell
pip install pip-tools
```

If you change one of the `*.in` files, you need to regenerate the concrete `requirements.txt` files as follows (the order is important):

```shell
pip-compile requirements.in
pip-compile test-requirements.in
```

By default, `pip-compile` doesn't update the pinned versions. This can be changed by adding the `--upgrade` flag to the above invocations:

```shell
pip-compile --upgrade requirements.in
pip-compile --upgrade test-requirements.in
```

Whenever the concrete `requirements.txt` file change you also shouldn't forget to re-run the `pip install -r ...` steps again.
6 changes: 6 additions & 0 deletions config.template.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
issuer: "did:web:example.com"
verification-methods:
keys:
- <path-to-key>
x509s:
- <url-to-cert>
Empty file added creator/__init__.py
Empty file.
47 changes: 47 additions & 0 deletions creator/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"""Script to generate DID documents fort did:web method.

(c) Anja Strunk <anja.sturnk@cloudandheat.com>, 4/2024
SPDX-License-Identifier: EPL-2.0
"""
import json

import click
import yaml

from creator import did_gen

DEFAULT_CONFIG_FILE = "/etc/scs-did-gen/config.yaml"


@click.command()
@click.option("--config", help="Configuration file for DID generator")
@click.option("--output-file", help="Output file - default stdout")
def did_creator(output_file, config):
"""Generates DID document for given DID and as set of verification methods defined in configuration file."""
if not config:
config = DEFAULT_CONFIG_FILE

with open(config, "r") as config_file:
config_dict = yaml.safe_load(config_file)
veri_meths = list()

# read out public keys
if "keys" in config_dict['verification-methods']:
for key in config_dict['verification-methods']['keys']:
veri_meths.append(did_gen.VerificationMethod(path=key))
# read out x509 certs
if "x509s" in config_dict['verification-methods']:
for cert in config_dict['verification-methods']['x509s']:
veri_meths.append(did_gen.VerificationMethod(path=cert, x509=True))

did_content = did_gen.generate_did_document(issuer=config_dict['issuer'], verification_methods=veri_meths)

if output_file:
with open(output_file, "w") as did_doc:
did_doc.write(json.dumps(did_content, indent=2))
else:
print(json.dumps(did_content, indent=2))


if __name__ == "__main__":
did_creator()
108 changes: 108 additions & 0 deletions creator/did_gen.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
"""Script to generate DID documents for did:web.

(c) Anja Strunk <anja.strunk@cloudandheat.com>, 4/2024
SPDX-License-Identifier: EPL-2.0
"""

import base64
from dataclasses import dataclass
from typing import List

import requests
from cryptography import x509
from cryptography.hazmat.primitives import serialization
from jwcrypto.jwt import JWK


@dataclass
class VerificationMethod:
path: str
x509: bool = False


CONTEXT = [
"https://www.w3.org/ns/did/v1",
"https://w3id.org/security/suites/jws-2020/v1"]
VM_CONTEXT = "https://w3c-ccg.github.io/lds-jws2020/contexts/v1/"
VM_TYPE = "JsonWebKey2020"


def generate_did_document(issuer: str, verification_methods: List[VerificationMethod]) -> dict:
""" Return a DID document for given issuer and with given verification methods as dict.

:param issuer: did:web of issuer
:type issuer: str
:param verification_methods: List of verification method to be added to DID document.
:type verification_methods: List of VerificationMethods
:@return: DID document as dict
:rtype dict
"""

did_doc = {
'@context': CONTEXT,
'id': issuer,
'verificationMethod': [],
'assertionMethod': [],
}

for key_number, m in enumerate(verification_methods):
if m.x509:
# parse x509 certificate
certs = _get_x509_content(m.path)
key_name = issuer + "#JWK2020-X509-" + str(key_number)
key = JWK.from_pem(certs[0].public_key().public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo))
else:
# parse public key
with open(m.path, mode="rb") as file:
key = JWK.from_pem(file.read())
if key.kty == "RSA":
key_name = issuer + "#JWK2020-RSA-" + str(key_number)
elif key.kty == "EC":
key_name = issuer + "#JWK2020-EC-" + str(key_number)
else:
raise ValueError("Unsupported JSON Web Key type: " + key.kty + " found.")

method = {
'@context': VM_CONTEXT,
'id': key_name,
'type': VM_TYPE,
'controller': issuer,
'publicKeyJwk': key.export(as_dict=True, private_key=False),
}

if m.x509:
if _is_url(m.path):
method['publicKeyJwk']['x5u'] = m.path
else:
method['publicKeyJwk']['x5c'] = _get_encode_cert_der_strings_b64(certs)
did_doc['verificationMethod'].append(method)
did_doc['assertionMethod'].append(key_name)
return did_doc


def _get_x509_content(path: str) -> List[x509.Certificate]:
if _is_url(path):
# a URL was given and we have to download certificate
response = requests.get(path)
if response.ok:
return x509.load_pem_x509_certificates(response.text.encode('utf-8'))
else:
raise requests.HTTPError(
'Could not retrieve x509 certificate from ' + path + ". (HTTP status code: " + response.status_code)
else:
# a path is given and have to load from file
with open(path, 'r') as file:
return x509.load_pem_x509_certificates(file.read().encode("utf-8"))


def _is_url(path: str) -> bool:
return path.startswith("http://") or path.startswith("https://")


def _get_encode_cert_der_strings_b64(cert_chain: x509.Certificate) -> List[str]:
return [
base64.b64encode(cert.public_bytes(encoding=serialization.Encoding.DER)).decode()
for cert in cert_chain
]
Loading
Loading