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

Prepare DID storage for multiple DIDs per DID Subject #3196

Merged
merged 16 commits into from
Jun 25, 2024
Merged
Show file tree
Hide file tree
Changes from 11 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
13 changes: 0 additions & 13 deletions docs/_static/vdr/v1.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -265,15 +265,6 @@ components:
deprecated: true
DIDCreateRequest:
properties:
controllers:
description: |
List of DIDs that can control the new DID Document. If selfControl = true and controllers is not empty,
the newly generated DID will be added to the list of controllers.
type: array
items:
type: string
description: DID according to Nuts specification
example: did:nuts:EwVMYK2ugaMvRHUbGFBhuyF423JuNQbtpes35eHhkQic
assertionMethod:
type: boolean
description: indicates if the generated key pair can be used for assertions.
Expand All @@ -297,10 +288,6 @@ components:
type: boolean
description: indicates if the generated key pair can be used for Key agreements.
default: true
selfControl:
type: boolean
description: whether the generated DID Document can be altered with its own capabilityInvocation key.
default: true
VerificationMethodRelationship:
properties:
assertionMethod:
Expand Down
21 changes: 4 additions & 17 deletions docs/_static/vdr/v2.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -370,25 +370,12 @@ components:
Options for the DID creation. If neither `did` nor `tenant` is given, a random UUID is used as tenant.
It's invalid to provide both `did` and `tenant` at the same time.
woutslakhorst marked this conversation as resolved.
Show resolved Hide resolved
properties:
did:
type: string
root:
type: boolean
woutslakhorst marked this conversation as resolved.
Show resolved Hide resolved
description: |
The DID of the DID document. If it's a did:web DID and it contains a path, it must follow the format `did:web:example.com:iam:1234`.
It can be used, for instance, to create a root did:web DID (one without path).
Can be used to create a root web:did.

The DID to create must conform to the node's configured URL (e.g., `did:web:example.com` for `https://example.com`).
example:
- "did:web:example.com"
- "did:web:example.com:iam:1234"
tenant:
type: string
description: |
The tenant of the DID document. If this option is given, the did:web DID will contain a path that ends with the given tenant ID.
It can be used, for instance, to serve multiple did:web DIDs from the same node.
example:
- "013c6fda-73e8-45ee-9220-48652dba854b"
- "library"
- "1234"
The DID created conforms to the node's configured URL (e.g., `did:web:example.com` for `https://example.com`).
DIDDocument:
$ref: '../common/ssi_types.yaml#/components/schemas/DIDDocument'
DIDDocumentMetadata:
Expand Down
3 changes: 2 additions & 1 deletion docs/pages/release_notes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Breaking changes
- Container user has changed from root to ``18081``. (see :ref:`running-docker`)
When migrating from v5, change the owner of the data directory on the host to that of the container's user. (``chown -R 18081:18081 /path/to/host/data-dir``)
- Docker image tags have been changed: previously version tags had were prefixed with ``v`` (e.g., ``v5.0.0``), this prefix has been dropped to better adhere to industry standards.
- The VDR v1 ``createDID`` (POST /internal/vdr/v1/did) no longer supports the ``controller`` and ``selfControl`` fields. All did:nuts documents are now self controlled. All existing documents will be migrated to self controlled at startup.
woutslakhorst marked this conversation as resolved.
Show resolved Hide resolved

============
New Features
Expand All @@ -35,7 +36,7 @@ The following new features have been added:
- Added support for OpenID4VCI (OpenID for Verifiable Credential Issuance)
- Added support for Nuts RFC021, which negotiates an OAuth2 access token for a system through a Presentation Exchange using Verifiable Credentials.
- Added support for `StatusList2021 <https://www.w3.org/TR/2023/WD-vc-status-list-20230427/>`_ as revocation means for Verifiable Credentials.
- Added support for storage in SQL databases (PostgreSQL, MySQL, SQLite and Microsoft SQL Server, see :ref:`storage-configuration`) for ``did:web``-related features.
- Added support for storage in SQL databases (PostgreSQL, MySQL, SQLite and Microsoft SQL Server, see :ref:`storage-configuration`) for various features.
- Added a Docker developer image (see :ref:`running-docker`) which is useful for local development. It is built from the latest ``master`` build.
- Added a VDR API operation to list locally managed DIDs.

Expand Down
13 changes: 3 additions & 10 deletions docs/pages/technology/did.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,14 @@ Only when information is added to the key pair, the key pair becomes important.
DIDs can gather claims through Verifiable Credentials. This allows a DID to actually represent something known in real life.
For example: adding an organization name credential connects the key pair to the name of the organization. It connects the digital world to the real world.

DID Documents
*************
Nuts DID Documents
******************

DIDs are backed by a *DID Document*. It defines the public keys, who can alter the document and any services related to the DID.
DID documents are automatically propagated through the network when they are created.
Nuts DID documents are automatically propagated through the network when they are created.
When DID documents are created, the DID **always** represents the public key fingerprint of the associated key.
A DID document is always created with a new key, the holder of the key can delegate the control to another DID.

Controller
==========

The controller of the DID document is the only one that can change the contents. It can assign other controllers, change keys, change services and revoke the DID.
When created, the DID document only has a single controller: the DID itself and the key related to it. It can choose to change add new controllers and remove existing ones.
Changes to DID documents are only accepted when the network transaction is signed with a controller's **authentication** key.

Verification Method
===================

Expand Down
16 changes: 8 additions & 8 deletions e2e-tests/browser/openid4vp_employeecredential/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@ func Test_UserAccessToken_EmployeeCredential(t *testing.T) {
cancel()
}()

didVerifier, openid4vpClientA := setupNode(t, ctx, "verifier", nodeAClientConfig)
didRequester, openid4vpClientB := setupNode(t, ctx, "requester", nodeBClientConfig)
didVerifier, openid4vpClientA := setupNode(t, ctx, nodeAClientConfig)
didRequester, openid4vpClientB := setupNode(t, ctx, nodeBClientConfig)
err := chromedp.Run(ctx, chromedp.Navigate("about:blank"))
require.NoError(t, err)
// Request an access token with user from verifying organization
Expand Down Expand Up @@ -94,7 +94,7 @@ func Test_UserAccessToken_EmployeeCredential(t *testing.T) {
require.NotEmpty(t, tokenInfo.Iat)
// Check the mapped input descriptor fields: for organization credential and employee credential
require.NotEmpty(t, tokenInfo.AdditionalProperties)
require.Equal(t, "requester Organization", tokenInfo.AdditionalProperties["organization_name"].(string))
require.Equal(t, fmt.Sprintf("%s Organization", didRequester.String()), tokenInfo.AdditionalProperties["organization_name"].(string))
require.Equal(t, "Testland", tokenInfo.AdditionalProperties["organization_city"].(string))
require.Equal(t, "jdoe@example.com", tokenInfo.AdditionalProperties["employee_identifier"].(string))
require.Equal(t, "John Doe", tokenInfo.AdditionalProperties["employee_name"].(string))
Expand All @@ -107,10 +107,10 @@ func Test_UserAccessToken_EmployeeCredential(t *testing.T) {
}
}

func setupNode(t testing.TB, ctx context.Context, id string, config core.ClientConfig) (did.DID, OpenID4VP) {
didDoc, err := createDID(id, config)
func setupNode(t testing.TB, ctx context.Context, config core.ClientConfig) (did.DID, OpenID4VP) {
didDoc, err := createDID(config)
require.NoError(t, err)
err = browser.IssueOrganizationCredential(didDoc, fmt.Sprintf("%s Organization", id), "Testland", config)
err = browser.IssueOrganizationCredential(didDoc, fmt.Sprintf("%s Organization", didDoc.ID.String()), "Testland", config)
require.NoError(t, err)

iamClientB, err := iamAPI.NewClient(config.GetAddress())
Expand All @@ -122,7 +122,7 @@ func setupNode(t testing.TB, ctx context.Context, id string, config core.ClientC
return didDoc.ID, openid4vp
}

func createDID(id string, config core.ClientConfig) (*did.Document, error) {
func createDID(config core.ClientConfig) (*did.Document, error) {
didClient := didAPI.HTTPClient{ClientConfig: config}
return didClient.Create(didAPI.CreateDIDOptions{Tenant: &id})
return didClient.Create(didAPI.CreateDIDOptions{})
}
39 changes: 25 additions & 14 deletions storage/sql_migrations/003_did.sql
Original file line number Diff line number Diff line change
Expand Up @@ -3,37 +3,48 @@
-- this table is used to store locally managed DIDs
create table did
(
-- did is the fully qualified DID
-- id is the fully qualified DID
id varchar(370) not null,
subject varchar(370) not null,
primary key (id)
);

create index did_subject_idx on did (subject);

create table did_document
woutslakhorst marked this conversation as resolved.
Show resolved Hide resolved
(
-- id is v4 uuid
id varchar(36) not null primary key,
did varchar(370) not null,
primary key (did)
version int not null,
unique (did, version),
foreign key (did) references did (id) on delete cascade
);

-- this table is used to store the verification methods for locally managed DIDs
create table did_verificationmethod
(
-- id is the unique id of the verification method as it appears in the DID document.
id varchar(415) not null,
-- did references the containing DID
did varchar(370) not null,
-- id is the unique id of the verification method as it appears in the DID document using the shorthand representation.
id varchar(254) not null primary key,
-- did_document_id references the DID document version
did_document_id varchar(36) not null,
-- data is a JSON object containing the verification method data, e.g. the public key.
-- When producing the verificationMethod, data is used as JSON base object and the id and type are added.
data $TEXT_TYPE not null,
primary key (id),
foreign key (did) references did (did) on delete cascade
foreign key (did_document_id) references did_document (id) on delete cascade
);

-- this table is used to store the services for locally managed DIDs
create table did_service
(
-- id is the unique id of the service as it appears in the DID document.
id varchar(415) not null,
-- did references the containing DID
did varchar(370) not null,
-- id is the unique id of the service as it appears in the DID document using the shorthand representation.
id varchar(254) not null primary key,
-- did_document_id references the DID document version
did_document_id varchar(36) not null,
-- data is a JSON object containing the service data, e.g. the serviceEndpoint.
-- When producing the service, data is used as JSON base object and the id and type are added.
data $TEXT_TYPE not null,
primary key (id),
foreign key (did) references did (did) on delete cascade
foreign key (did_document_id) references did_document (id) on delete cascade
);

-- +goose Down
Expand Down
2 changes: 1 addition & 1 deletion storage/sql_migrations/005_statuslist.sql
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ create table status_list
-- last_issued_index: the highest status_list_index issued for this page. Should not be incremented above the max statusListIndex.
last_issued_index integer not null,
-- Ties status list credentials to DID management.
constraint fk_issuer_did foreign key (issuer) references did (did) on delete cascade
constraint fk_issuer_did foreign key (issuer) references did (id) on delete cascade
);

-- status_list_entry: lists all status list entries for which the status bit is set to true. (revocation table)
Expand Down
4 changes: 2 additions & 2 deletions storage/test.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ func NewTestStorageRedisEngineInDir(t testing.TB, dir string) (Engine, *miniredi

func NewTestStorageEngineInDir(t testing.TB, dir string) Engine {
result := New().(*engine)
// Prevent dbmate and gorm from logging database creation and applied schema migrations.
// Prevent goose and gorm from logging database creation and applied schema migrations.
// These are logged on INFO, which is good for production but annoying in unit tests.
result.sqlMigrationLogger = nilGooseLogger{}

Expand Down Expand Up @@ -127,7 +127,7 @@ func NewTestInMemorySessionDatabase(t *testing.T) *InMemorySessionDatabase {
func AddDIDtoSQLDB(t testing.TB, db *gorm.DB, dids ...did.DID) {
for _, id := range dids {
// use gorm EXEC since it accepts '?' as the argument placeholder for all DBs
require.NoError(t, db.Exec("INSERT INTO did ( did ) VALUES ( ? )", id.String()).Error)
require.NoError(t, db.Exec("INSERT INTO did ( subject, id ) VALUES ( ?, ? )", id.String(), id.String()).Error)
}
}

Expand Down
15 changes: 1 addition & 14 deletions vdr/api/v1/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,25 +116,12 @@ func (a *Wrapper) Routes(router core.EchoRouter) {
// CreateDID creates a new DID Document and returns it.
func (a *Wrapper) CreateDID(ctx context.Context, request CreateDIDRequestObject) (CreateDIDResponseObject, error) {
options := didnuts.DefaultCreationOptions()
if request.Body.Controllers != nil {
var controllers []did.DID
for _, c := range *request.Body.Controllers {
id, err := did.ParseDID(c)
if err != nil {
return nil, core.InvalidInputError("controller entry (%s) could not be parsed: %w", c, err)
}
controllers = append(controllers, *id)
}
options = options.With(didnuts.Controllers(controllers...))
}

defaultKeyFlags := didnuts.DefaultKeyFlags()
keyFlags := request.Body.VerificationMethodRelationship.ToFlags(defaultKeyFlags)
if keyFlags != defaultKeyFlags {
options = options.With(keyFlags)
}
if request.Body.SelfControl != nil {
options = options.With(didnuts.SelfControl(*request.Body.SelfControl))
}

doc, _, err := a.VDR.Create(ctx, options)
// if this operation leads to an error, it may return a 500
Expand Down
14 changes: 0 additions & 14 deletions vdr/api/v1/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,20 +79,6 @@ func TestWrapper_CreateDID(t *testing.T) {
assert.Equal(t, *id, response.(CreateDID200JSONResponse).ID)
})

t.Run("error - invalid controller DID", func(t *testing.T) {
ctx := newMockContext(t)
controllers := []string{"not_a_did"}
request := DIDCreateRequest{
Controllers: &controllers,
}

response, err := ctx.client.CreateDID(nil, CreateDIDRequestObject{Body: &request})

assert.ErrorIs(t, err, did.ErrInvalidDID)
assert.Equal(t, http.StatusBadRequest, ctx.client.ResolveStatusCode(err))
assert.Nil(t, response)
})

t.Run("error - create fails", func(t *testing.T) {
ctx := newMockContext(t)
request := DIDCreateRequest{}
Expand Down
11 changes: 2 additions & 9 deletions vdr/api/v2/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,15 +72,8 @@ func (w *Wrapper) Routes(router core.EchoRouter) {

func (w *Wrapper) CreateDID(ctx context.Context, request CreateDIDRequestObject) (CreateDIDResponseObject, error) {
options := management.Create(didweb.MethodName)
if request.Body.Tenant != nil && *request.Body.Tenant != "" {
options = options.With(didweb.Tenant(*request.Body.Tenant))
}
if request.Body.Did != nil && *request.Body.Did != "" {
newDID, err := did.ParseDID(*request.Body.Did)
if err != nil {
return nil, err
}
options = options.With(didweb.DID(*newDID))
if request.Body.Root != nil && *request.Body.Root {
options = options.With(didweb.RootDID())
}

doc, _, err := w.VDR.Create(ctx, options)
Expand Down
23 changes: 4 additions & 19 deletions vdr/api/v2/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,30 +50,15 @@ func TestWrapper_CreateDID(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, id, response.(CreateDID200JSONResponse).ID)
})
t.Run("with tenant", func(t *testing.T) {
t.Run("as Root", func(t *testing.T) {
ctx := newMockContext(t)
opts := didweb.DefaultCreationOptions().With(didweb.Tenant("1"))
opts := didweb.DefaultCreationOptions()
ctx.vdr.EXPECT().Create(gomock.Any(), opts).Return(&didDoc, nil, nil)

var tenant = "1"
root := false
response, err := ctx.client.CreateDID(nil, CreateDIDRequestObject{
Body: &CreateDIDJSONRequestBody{
Tenant: &tenant,
},
})

require.NoError(t, err)
assert.Equal(t, id, response.(CreateDID200JSONResponse).ID)
})
t.Run("with DID", func(t *testing.T) {
ctx := newMockContext(t)
opts := didweb.DefaultCreationOptions().With(didweb.DID(id))
ctx.vdr.EXPECT().Create(gomock.Any(), opts).Return(&didDoc, nil, nil)

var newDID = id.String()
response, err := ctx.client.CreateDID(nil, CreateDIDRequestObject{
Body: &CreateDIDJSONRequestBody{
Did: &newDID,
Root: &root,
},
})

Expand Down
11 changes: 3 additions & 8 deletions vdr/api/v2/generated.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading