Skip to content

Commit

Permalink
extended goldenhammer with key alaising
Browse files Browse the repository at this point in the history
  • Loading branch information
woutslakhorst committed Nov 28, 2023
1 parent a698c71 commit 326d68e
Show file tree
Hide file tree
Showing 12 changed files with 163 additions and 18 deletions.
10 changes: 9 additions & 1 deletion auth/api/iam/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,7 @@ func (r Wrapper) OAuthAuthorizationServerMetadata(ctx context.Context, request O

func (r Wrapper) GetWebDID(ctx context.Context, request GetWebDIDRequestObject) (GetWebDIDResponseObject, error) {
baseURL := *(r.auth.PublicURL().JoinPath(apiPath))
ownDID := r.idToDID(request.Id)
ownDID := idToNutsDID(request.Id)

document, err := r.vdr.DeriveWebDIDDocument(ctx, baseURL, ownDID)
if err != nil {
Expand Down Expand Up @@ -400,3 +400,11 @@ func (r Wrapper) idToDID(id string) did.DID {
did, _ := didweb.URLToDID(*url)
return *did
}

func idToNutsDID(id string) did.DID {
return did.DID{
Method: "nuts",
ID: id,
DecodedID: id,
}
}
8 changes: 4 additions & 4 deletions auth/api/iam/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ func TestWrapper_OAuthAuthorizationServerMetadata(t *testing.T) {
}

func TestWrapper_GetWebDID(t *testing.T) {
webDID := did.MustParseDID("did:web:example.com:iam:123")
nutsDID := did.MustParseDID("did:nuts:123")
publicURL := ssi.MustParseURI("https://example.com").URL
webDIDBaseURL := publicURL.JoinPath("/iam")
ctx := audit.TestContext()
Expand All @@ -112,7 +112,7 @@ func TestWrapper_GetWebDID(t *testing.T) {

t.Run("ok", func(t *testing.T) {
test := newTestClient(t)
test.vdr.EXPECT().DeriveWebDIDDocument(gomock.Any(), *webDIDBaseURL, webDID).Return(&expectedWebDIDDoc, nil)
test.vdr.EXPECT().DeriveWebDIDDocument(gomock.Any(), *webDIDBaseURL, nutsDID).Return(&expectedWebDIDDoc, nil)

response, err := test.client.GetWebDID(ctx, GetWebDIDRequestObject{webID})

Expand All @@ -121,7 +121,7 @@ func TestWrapper_GetWebDID(t *testing.T) {
})
t.Run("unknown DID", func(t *testing.T) {
test := newTestClient(t)
test.vdr.EXPECT().DeriveWebDIDDocument(ctx, *webDIDBaseURL, webDID).Return(nil, resolver.ErrNotFound)
test.vdr.EXPECT().DeriveWebDIDDocument(ctx, *webDIDBaseURL, nutsDID).Return(nil, resolver.ErrNotFound)

response, err := test.client.GetWebDID(ctx, GetWebDIDRequestObject{webID})

Expand All @@ -130,7 +130,7 @@ func TestWrapper_GetWebDID(t *testing.T) {
})
t.Run("other error", func(t *testing.T) {
test := newTestClient(t)
test.vdr.EXPECT().DeriveWebDIDDocument(gomock.Any(), *webDIDBaseURL, webDID).Return(nil, errors.New("failed"))
test.vdr.EXPECT().DeriveWebDIDDocument(gomock.Any(), *webDIDBaseURL, nutsDID).Return(nil, errors.New("failed"))

response, err := test.client.GetWebDID(ctx, GetWebDIDRequestObject{webID})

Expand Down
2 changes: 1 addition & 1 deletion cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ func CreateSystem(shutdownCallback context.CancelFunc) *core.System {
authInstance := auth.NewAuthInstance(auth.DefaultConfig(), vdrInstance, credentialInstance, cryptoInstance, didmanInstance, jsonld, pkiInstance)
statusEngine := status.NewStatusEngine(system)
metricsEngine := core.NewMetricsEngine()
goldenHammer := golden_hammer.New(vdrInstance, didmanInstance)
goldenHammer := golden_hammer.New(vdrInstance, didmanInstance, cryptoInstance, authInstance)

// Register HTTP routes
system.RegisterRoutes(&core.LandingPage{})
Expand Down
14 changes: 14 additions & 0 deletions crypto/crypto.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,20 @@ func (client *Crypto) New(ctx context.Context, namingFunc KIDNamingFunc) (Key, e
}, nil
}

func (client *Crypto) Alias(ctx context.Context, kid string, alias string) error {
if client.storage.PrivateKeyExists(ctx, alias) {
return nil
}
keypair, err := client.storage.GetPrivateKey(ctx, kid)
if err != nil {
return err
}
if err = client.storage.SavePrivateKey(ctx, alias, keypair); err != nil {
return fmt.Errorf("could not alias private key: %w", err)
}
return nil
}

func generateKeyPairAndKID(namingFunc KIDNamingFunc) (*ecdsa.PrivateKey, string, error) {
keyPair, err := generateECKeyPair()
if err != nil {
Expand Down
2 changes: 2 additions & 0 deletions crypto/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ type KeyCreator interface {
// New generates a keypair and returns a Key. The context is used to pass audit information.
// The KIDNamingFunc will provide the kid.
New(ctx context.Context, namingFunc KIDNamingFunc) (Key, error)
// Alias creates an alias for the given KID. The context is used to pass audit information.
Alias(ctx context.Context, kid string, alias string) error
}

// KeyResolver is the interface for resolving keys.
Expand Down
28 changes: 28 additions & 0 deletions crypto/mock.go

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

12 changes: 11 additions & 1 deletion e2e-tests/oauth-flow/openid4vp/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ services:
- "./node-A/data:/opt/nuts/data:rw"
- "../../tls-certs/nodeA-backend-certificate.pem:/opt/nuts/certificate-and-key.pem:ro"
- "../../tls-certs/truststore.pem:/opt/nuts/truststore.pem:ro"
- "../../tls-certs/truststore.pem:/etc/ssl/certs/truststore.pem:ro"
- "./node-A/presentationexchangemapping.json:/opt/nuts/presentationexchangemapping.json:ro"
healthcheck:
interval: 1s # Make test run quicker by checking health status more often
Expand All @@ -24,7 +25,7 @@ services:
- "../../tls-certs/nodeA-certificate.pem:/etc/nginx/ssl/key.pem:ro"
- "../../tls-certs/truststore.pem:/etc/nginx/ssl/truststore.pem:ro"
- "./node-A/html:/etc/nginx/html:ro"
nodeB:
nodeB-backend:
image: "${IMAGE_NODE_B:-nutsfoundation/nuts-node:master}"
ports:
- "21323:1323"
Expand All @@ -38,3 +39,12 @@ services:
- "../../tls-certs/truststore.pem:/etc/ssl/certs/truststore.pem:ro"
healthcheck:
interval: 1s # Make test run quicker by checking health status more often
nodeB:
image: nginx:1.25.1
ports:
- "20443:443"
volumes:
- "./node-B/nginx.conf:/etc/nginx/nginx.conf:ro"
- "../../tls-certs/nodeB-certificate.pem:/etc/nginx/ssl/server.pem:ro"
- "../../tls-certs/nodeB-certificate.pem:/etc/nginx/ssl/key.pem:ro"
- "../../tls-certs/truststore.pem:/etc/nginx/ssl/truststore.pem:ro"
2 changes: 2 additions & 0 deletions e2e-tests/oauth-flow/openid4vp/node-A/nuts.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,5 @@ tls:
truststorefile: /opt/nuts/truststore.pem
certfile: /opt/nuts/certificate-and-key.pem
certkeyfile: /opt/nuts/certificate-and-key.pem
goldenhammer:
interval: 1s
47 changes: 47 additions & 0 deletions e2e-tests/oauth-flow/openid4vp/node-B/nginx.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
user nginx;
worker_processes 1;

error_log /var/log/nginx/error.log debug;
pid /var/run/nginx.pid;


events {
worker_connections 1024;
}


http {
include /etc/nginx/mime.types;
default_type application/octet-stream;

log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';

access_log /var/log/nginx/access.log main;

keepalive_timeout 65;

include /etc/nginx/conf.d/*.conf;

upstream nodeB-backend {
server nodeB-backend:1323;
}

server {
server_name nodeA;
listen 443 ssl;
http2 on;
ssl_certificate /etc/nginx/ssl/server.pem;
ssl_certificate_key /etc/nginx/ssl/key.pem;
ssl_client_certificate /etc/nginx/ssl/truststore.pem;
ssl_verify_client optional;
ssl_verify_depth 1;
ssl_protocols TLSv1.3;

location / {
proxy_set_header X-Ssl-Client-Cert $ssl_client_escaped_cert;
proxy_pass http://nodeB-backend;
}
}
}
3 changes: 2 additions & 1 deletion e2e-tests/oauth-flow/openid4vp/node-B/nuts.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@ tls:
truststorefile: /opt/nuts/truststore.pem
certfile: /opt/nuts/certificate-and-key.pem
certkeyfile: /opt/nuts/certificate-and-key.pem

goldenhammer:
interval: 1s
19 changes: 11 additions & 8 deletions e2e-tests/oauth-flow/openid4vp/run-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -31,19 +31,25 @@ DIDDOC_HASH=$(docker compose exec nodeA-backend nuts vdr resolve $VENDOR_A_DID -
docker compose exec nodeA-backend nuts vdr update "${VENDOR_A_DID}" "${DIDDOC_HASH}" /opt/nuts/data/updated-did.json

# Register Vendor B
VENDOR_B_DIDDOC=$(docker compose exec nodeB nuts vdr create-did)
VENDOR_B_DIDDOC=$(docker compose exec nodeB-backend nuts vdr create-did)
VENDOR_B_DID=$(echo $VENDOR_B_DIDDOC | jq -r .id)
echo Vendor B DID: $VENDOR_B_DID
# Add assertionMethod
VENDOR_B_KEYID=$(echo $VENDOR_B_DIDDOC | jq -r '.verificationMethod[0].id')
VENDOR_B_DIDDOC=$(echo $VENDOR_B_DIDDOC | jq ". |= . + {assertionMethod: [\"${VENDOR_B_KEYID}\"]}")
# Perform update
echo $VENDOR_B_DIDDOC > ./node-B/data/updated-did.json
DIDDOC_HASH=$(docker compose exec nodeB nuts vdr resolve $VENDOR_B_DID --metadata | jq -r .hash)
docker compose exec nodeB nuts vdr update "${VENDOR_B_DID}" "${DIDDOC_HASH}" /opt/nuts/data/updated-did.json
DIDDOC_HASH=$(docker compose exec nodeB-backend nuts vdr resolve $VENDOR_B_DID --metadata | jq -r .hash)
docker compose exec nodeB-backend nuts vdr update "${VENDOR_B_DID}" "${DIDDOC_HASH}" /opt/nuts/data/updated-did.json

# wait for golden hammer to alias keys
sleep 5

# Issue NutsOrganizationCredential for Vendor B
REQUEST="{\"type\":\"NutsOrganizationCredential\",\"issuer\":\"${VENDOR_B_DID}\", \"credentialSubject\": {\"id\":\"${VENDOR_B_DID}\", \"organization\":{\"name\":\"Caresoft B.V.\", \"city\":\"Caretown\"}},\"visibility\": \"public\"}"
# Create DID for A/B with :nuts: replaced with :web:
VENDOR_A_DID_WEB=$(echo $VENDOR_A_DID | sed 's/:nuts/:web:nodeA:iam/')
VENDOR_B_DID_WEB=$(echo $VENDOR_B_DID | sed 's/:nuts/:web:nodeB:iam/')
REQUEST="{\"type\":\"NutsOrganizationCredential\",\"issuer\":\"${VENDOR_B_DID}\", \"credentialSubject\": {\"id\":\"${VENDOR_B_DID_WEB}\", \"organization\":{\"name\":\"Caresoft B.V.\", \"city\":\"Caretown\"}},\"visibility\": \"public\"}"
RESPONSE=$(echo $REQUEST | curl -X POST --data-binary @- http://localhost:21323/internal/vcr/v2/issuer/vc -H "Content-Type:application/json")
if echo $RESPONSE | grep -q "VerifiableCredential"; then
echo "VC issued"
Expand All @@ -57,11 +63,8 @@ echo "---------------------------------------"
echo "Perform OAuth 2.0 OpenID4VP-s2s flow..."
echo "---------------------------------------"
# Request access token
# Create DID for A with :nuts: replaced with :web:
VENDOR_A_DID_WEB=$(echo $VENDOR_A_DID | sed 's/:nuts/:web:nodeA:iam/')
VENDOR_B_DID_WEB=$(echo $VENDOR_B_DID | sed 's/:nuts/:web:nodeB:iam/')
REQUEST="{\"verifier\":\"${VENDOR_A_DID_WEB}\",\"scope\":\"test\"}"
RESPONSE=$(echo $REQUEST | curl -X POST -s --data-binary @- http://localhost:21323/internal/auth/v2/$VENDOR_B_DID/request-access-token -H "Content-Type:application/json" -v)
RESPONSE=$(echo $REQUEST | curl -X POST -s --data-binary @- http://localhost:21323/internal/auth/v2/$VENDOR_B_DID_WEB/request-access-token -H "Content-Type:application/json" -v)
#if echo $RESPONSE | grep -q "access_token"; then
# echo $RESPONSE | sed -E 's/.*"access_token":"([^"]*).*/\1/' > ./node-B/data/accesstoken.txt
# echo "access token stored in ./node-B/data/accesstoken.txt"
Expand Down
34 changes: 32 additions & 2 deletions golden_hammer/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,12 @@ package golden_hammer
import (
"context"
"crypto/tls"
"fmt"
"github.com/nuts-foundation/go-did/did"
"github.com/nuts-foundation/nuts-node/audit"
"github.com/nuts-foundation/nuts-node/auth"
"github.com/nuts-foundation/nuts-node/core"
"github.com/nuts-foundation/nuts-node/crypto"
"github.com/nuts-foundation/nuts-node/didman"
"github.com/nuts-foundation/nuts-node/golden_hammer/log"
"github.com/nuts-foundation/nuts-node/network/transport"
Expand All @@ -43,9 +46,11 @@ var _ core.Named = (*GoldenHammer)(nil)
var _ core.Configurable = (*GoldenHammer)(nil)
var _ core.Injectable = (*GoldenHammer)(nil)

func New(vdrInstance vdr.VDR, didmanAPI didman.Didman) *GoldenHammer {
func New(vdrInstance vdr.VDR, didmanAPI didman.Didman, cryptoInstance crypto.KeyStore, authInstance auth.AuthenticationServices) *GoldenHammer {
return &GoldenHammer{
routines: &sync.WaitGroup{},
authInstance: authInstance,
cryptoInstance: cryptoInstance,
vdrInstance: vdrInstance,
didmanAPI: didmanAPI,
fixedDocumentDIDs: map[string]bool{},
Expand All @@ -61,6 +66,8 @@ type GoldenHammer struct {
ctx context.Context
cancelFunc context.CancelFunc
routines *sync.WaitGroup
authInstance auth.AuthenticationServices
cryptoInstance crypto.KeyStore
didmanAPI didman.Didman
vdrInstance vdr.VDR
fixedDocumentDIDs map[string]bool
Expand Down Expand Up @@ -114,10 +121,33 @@ func (h *GoldenHammer) hammerTime() {
case <-ticker.C:
err := h.registerServiceBaseURLs()
if err != nil {
log.Logger().WithError(err).Warn("Auto-fix error")
log.Logger().WithError(err).Warn("ServiceBaseURL auto-fix error")
}
err = h.copyKeysForDIDWeb()
if err != nil {
log.Logger().WithError(err).Warn("DIDWeb key auto-fix error")
}
}
}
}

func (h *GoldenHammer) copyKeysForDIDWeb() error {
keyIds := h.cryptoInstance.List(h.ctx)
publicURL := h.authInstance.PublicURL()
if publicURL != nil {
for _, keyId := range keyIds {
if strings.HasPrefix(keyId, "did:nuts") {
// replace did:nuts with did:web:publicURL:iam
newKeyId := strings.Replace(keyId, "did:nuts", fmt.Sprintf("did:web:%s:iam", publicURL.Hostname()), 1)
log.Logger().Infof("Copying key %s to %s", keyId, newKeyId)
err := h.cryptoInstance.Alias(h.ctx, keyId, newKeyId)
if err != nil {
log.Logger().WithError(err).Warnf("Unable to alias key %s to %s", keyId, newKeyId)
}
}
}
}
return nil
}

// registerServiceBaseURLs registers the node's services HTTP base URL on its DIDs.
Expand Down

0 comments on commit 326d68e

Please sign in to comment.