Skip to content

Commit

Permalink
feat: Signed credential offer for Issuance (#1407)
Browse files Browse the repository at this point in the history
Signed-off-by: Mykhailo Sizov <[email protected]>
  • Loading branch information
mishasizov-SK authored Sep 8, 2023
1 parent 368c3c1 commit 9833912
Show file tree
Hide file tree
Showing 16 changed files with 729 additions and 54 deletions.
6 changes: 6 additions & 0 deletions cmd/vc-rest/startcmd/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -657,6 +657,8 @@ func buildEchoHandler(
PreAuthCodeTTL: conf.StartupParameters.transientDataParams.claimDataTTL,
CredentialOfferReferenceStore: credentialOfferStore,
DataProtector: claimsDataProtector,
KMSRegistry: kmsRegistry,
CryptoJWTSigner: vcCrypto,
})
if err != nil {
return nil, fmt.Errorf("failed to instantiate new oidc4ci service: %w", err)
Expand Down Expand Up @@ -932,6 +934,10 @@ type credentialOfferReferenceStore interface {
ctx context.Context,
request *oidc4ci.CredentialOfferResponse,
) (string, error)
CreateJWT(
ctx context.Context,
credentialOfferJWT string,
) (string, error)
}

func getOIDC4VPClaimsStore(
Expand Down
68 changes: 63 additions & 5 deletions component/wallet-cli/pkg/credentialoffer/credentialoffer.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,39 @@ package credentialoffer
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"

"github.com/hyperledger/aries-framework-go/component/models/jwt"
"github.com/hyperledger/aries-framework-go/component/models/verifiable"
vdrapi "github.com/hyperledger/aries-framework-go/component/vdr/api"
"github.com/valyala/fastjson"

"github.com/trustbloc/vcs/pkg/service/oidc4ci"
)

func ParseInitiateIssuanceUrl(rawURL string, client *http.Client) (*oidc4ci.CredentialOfferResponse, error) {
func ParseInitiateIssuanceUrl(rawURL string, client *http.Client, vdrRegistry vdrapi.Registry) (*oidc4ci.CredentialOfferResponse, error) {
initiateIssuanceURLParsed, err := url.Parse(rawURL)
if err != nil {
return nil, fmt.Errorf("failed to parse url %w", err)
}

credentialOfferURL := initiateIssuanceURLParsed.Query().Get("credential_offer")
var offerResponse oidc4ci.CredentialOfferResponse
var credentialOfferPayload []byte

if credentialOfferQueryParam := initiateIssuanceURLParsed.Query().Get("credential_offer"); len(credentialOfferQueryParam) > 0 {
credentialOfferPayload = []byte(credentialOfferQueryParam)
// Depends on Issuer configuration, credentialOfferURL might be either JWT signed CredentialOfferResponse,
// or encoded oidc4ci.CredentialOfferResponse itself.
if jwt.IsJWS(credentialOfferQueryParam) {
credentialOfferPayload, err = getCredentialOfferJWTPayload(credentialOfferQueryParam, vdrRegistry)
if err != nil {
return nil, err
}
}

if len(credentialOfferURL) > 0 {
if err = json.Unmarshal([]byte(credentialOfferURL), &offerResponse); err != nil {
if err = json.Unmarshal(credentialOfferPayload, &offerResponse); err != nil {
return nil, fmt.Errorf("can not parse credential offer. %w", err)
}

Expand All @@ -37,9 +53,51 @@ func ParseInitiateIssuanceUrl(rawURL string, client *http.Client) (*oidc4ci.Cred
}

defer resp.Body.Close()
if err = json.NewDecoder(resp.Body).Decode(&offerResponse); err != nil {

credentialOfferPayload, err = io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("read credential_offer_uriresponse body: %w", err)
}

// Depends on Issuer configuration, rspBody might be either JWT signed CredentialOfferResponse,
// or encoded oidc4ci.CredentialOfferResponse itself.
if jwt.IsJWS(string(credentialOfferPayload)) {
credentialOfferPayload, err = getCredentialOfferJWTPayload(string(credentialOfferPayload), vdrRegistry)
if err != nil {
return nil, err
}
}

if err = json.Unmarshal(credentialOfferPayload, &offerResponse); err != nil {
return nil, err
}

return &offerResponse, nil
}

func getCredentialOfferJWTPayload(rawResponse string, vdrRegistry vdrapi.Registry) ([]byte, error) {
jwtVerifier := jwt.NewVerifier(jwt.KeyResolverFunc(
verifiable.NewVDRKeyResolver(vdrRegistry).PublicKeyFetcher()))

_, credentialOfferPayload, err := jwt.Parse(
rawResponse,
jwt.WithSignatureVerifier(jwtVerifier),
jwt.WithIgnoreClaimsMapDecoding(true),
)
if err != nil {
return nil, fmt.Errorf("parse credential offer JWT: %w", err)
}

var fastParser fastjson.Parser
v, err := fastParser.ParseBytes(credentialOfferPayload)
if err != nil {
return nil, fmt.Errorf("decode claims: %w", err)
}

sb, err := v.Get("credential_offer").Object()
if err != nil {
return nil, fmt.Errorf("fastjson.Parser Get credential_offer: %w", err)
}

return sb.MarshalTo([]byte{}), nil
}
12 changes: 7 additions & 5 deletions component/wallet-cli/pkg/walletrunner/wallet_runner_oidc4ci.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,16 @@ func (s *Service) RunOIDC4CI(config *OIDC4CIConfig, hooks *Hooks) error {
log.Println("Starting OIDC4VCI authorized code flow")
ctx := context.Background()
log.Printf("Initiate issuance URL:\n\n\t%s\n\n", config.InitiateIssuanceURL)

err := s.CreateWallet()
if err != nil {
return fmt.Errorf("create wallet: %w", err)
}

offerResponse, err := credentialoffer.ParseInitiateIssuanceUrl(
config.InitiateIssuanceURL,
s.httpClient,
s.ariesServices.vdrRegistry,
)
if err != nil {
return fmt.Errorf("parse initiate issuance url: %w", err)
Expand Down Expand Up @@ -193,11 +200,6 @@ func (s *Service) RunOIDC4CI(config *OIDC4CIConfig, hooks *Hooks) error {

s.token = token

err = s.CreateWallet()
if err != nil {
return fmt.Errorf("create wallet: %w", err)
}

s.print("Getting credential")
vc, _, err := s.getCredential(
oidcIssuerCredentialConfig.CredentialEndpoint,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,26 @@ func (s *Service) RunOIDC4CIPreAuth(config *OIDC4CIConfig) (*verifiable.Credenti
log.Println("Starting OIDC4VCI pre-authorized code flow")

ctx := context.Background()

startTime := time.Now()
err := s.CreateWallet()
if err != nil {
return nil, fmt.Errorf("failed to create wallet: %w", err)
}
s.perfInfo.CreateWallet = time.Since(startTime)

log.Printf("Initiate issuance URL:\n\n\t%s\n\n", config.InitiateIssuanceURL)
offerResponse, err := credentialoffer.ParseInitiateIssuanceUrl(config.InitiateIssuanceURL, s.httpClient)
offerResponse, err := credentialoffer.ParseInitiateIssuanceUrl(
config.InitiateIssuanceURL,
s.httpClient,
s.ariesServices.vdrRegistry,
)
if err != nil {
return nil, fmt.Errorf("parse initiate issuance url: %w", err)
}

s.print("Getting issuer OIDC config")
startTime := time.Now()
startTime = time.Now()
oidcConfig, err := s.getIssuerOIDCConfig(ctx, offerResponse.CredentialIssuer)
s.perfInfo.VcsCIFlowDuration += time.Since(startTime) // oidc config
s.perfInfo.GetIssuerOIDCConfig = time.Since(startTime)
Expand Down Expand Up @@ -106,13 +118,6 @@ func (s *Service) RunOIDC4CIPreAuth(config *OIDC4CIConfig) (*verifiable.Credenti
"c_nonce": *token.CNonce,
})

startTime = time.Now()
err = s.CreateWallet()
if err != nil {
return nil, fmt.Errorf("failed to create wallet: %w", err)
}
s.perfInfo.CreateWallet = time.Since(startTime)

s.print("Getting credential")
startTime = time.Now()
vc, vcsDuration, err := s.getCredential(credentialsEndpoint, config.CredentialType, config.CredentialFormat,
Expand Down
31 changes: 31 additions & 0 deletions pkg/doc/vc/crypto/crypto.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ import (

"github.com/piprate/json-gold/ld"

"github.com/hyperledger/aries-framework-go/component/kmscrypto/doc/jose"
"github.com/hyperledger/aries-framework-go/component/models/did"
"github.com/hyperledger/aries-framework-go/component/models/jwt"
ldprocessor "github.com/hyperledger/aries-framework-go/component/models/ld/processor"
ariessigner "github.com/hyperledger/aries-framework-go/component/models/signature/signer"
"github.com/hyperledger/aries-framework-go/component/models/signature/suite"
Expand Down Expand Up @@ -168,6 +170,35 @@ func (c *Crypto) SignCredential(
}
}

// NewJWTSigned returns JWT signed claims.
func (c *Crypto) NewJWTSigned(claims interface{}, signerData *vc.Signer) (string, error) {
jwsAlgo, err := verifiable.KeyTypeToJWSAlgo(signerData.KeyType)
if err != nil {
return "", fmt.Errorf("getting JWS algo based on signature type: %w", err)
}

jwtAlgoStr, err := jwsAlgo.Name()
if err != nil {
return "", fmt.Errorf("get jwt algo name: %w", err)
}

signer, _, err := c.getSigner(signerData.KMSKeyID, signerData.KMS, signerData.SignatureType)
if err != nil {
return "", err
}

headers := map[string]interface{}{
jose.HeaderKeyID: signerData.Creator,
}

token, err := jwt.NewSigned(claims, headers, verifiable.GetJWTSigner(signer, jwtAlgoStr))
if err != nil {
return "", fmt.Errorf("newSigned: %w", err)
}

return token.Serialize(false)
}

// signCredentialLDP adds verifiable.LinkedDataProofContext to the VC.
func (c *Crypto) signCredentialLDP(
signerData *vc.Signer, vc *verifiable.Credential, opts ...SigningOpts) (*verifiable.Credential, error) {
Expand Down
118 changes: 118 additions & 0 deletions pkg/doc/vc/crypto/crypto_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import (
vdrmock "github.com/hyperledger/aries-framework-go/component/vdr/mock"
ariescrypto "github.com/hyperledger/aries-framework-go/spi/crypto"
"github.com/hyperledger/aries-framework-go/spi/kms"
"github.com/piprate/json-gold/ld"
"github.com/stretchr/testify/require"

"github.com/trustbloc/vcs/pkg/doc/vc"
Expand Down Expand Up @@ -819,6 +820,7 @@ func getTestLDPSigner() *vc.Signer {
SignatureType: "Ed25519Signature2018",
Creator: "did:trustbloc:abc#key1",
KMSKeyID: "key1",
KeyType: kms.ED25519,
KMS: &mockVCSKeyManager{
crypto: &cryptomock.Crypto{},
kms: &mockkms.KeyManager{},
Expand Down Expand Up @@ -881,12 +883,17 @@ func createKMS(t *testing.T) *localkms.LocalKMS {
}

type mockVCSKeyManager struct {
err error
crypto ariescrypto.Crypto
kms kms.KeyManager
}

func (m *mockVCSKeyManager) NewVCSigner(creator string,
signatureType vcsverifiable.SignatureType) (vc.SignerAlgorithm, error) {
if m.err != nil {
return nil, m.err
}

return signer.NewKMSSigner(m.kms, m.crypto, creator, signatureType, nil)
}

Expand Down Expand Up @@ -938,3 +945,114 @@ func createDIDDoc(didID string, opts ...opt) *did.Doc {
CapabilityDelegation: []did.Verification{{VerificationMethod: signingKey}},
}
}

func TestCrypto_NewJWTSigned(t *testing.T) {
testClaims := map[string]interface{}{
"key": "value",
}

type fields struct {
vdr vdrapi.Registry
documentLoader ld.DocumentLoader
}
type args struct {
getClaims func() interface{}
getSignerData func() *vc.Signer
}
tests := []struct {
name string
fields fields
args args
want string
wantErr bool
}{
{
name: "Success",
fields: fields{
vdr: &vdrmock.VDRegistry{ResolveValue: createDIDDoc("did:trustbloc:abc")},
documentLoader: testutil.DocumentLoader(t),
},
args: args{
getClaims: func() interface{} {
return testClaims
},
getSignerData: getTestLDPSigner,
},
want: "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDp0cnVzdGJsb2M6YWJjI2tleTEifQ.eyJrZXkiOiJ2YWx1ZSJ9.",
wantErr: false,
},
{
name: "Error KeyTypeToJWSAlgo",
fields: fields{
vdr: &vdrmock.VDRegistry{ResolveValue: createDIDDoc("did:trustbloc:abc")},
documentLoader: testutil.DocumentLoader(t),
},
args: args{
getClaims: func() interface{} {
return testClaims
},
getSignerData: func() *vc.Signer {
s := getTestLDPSigner()
s.KeyType = ""

return s
},
},
want: "",
wantErr: true,
},
{
name: "Error getSigner",
fields: fields{
vdr: &vdrmock.VDRegistry{ResolveValue: createDIDDoc("did:trustbloc:abc")},
documentLoader: testutil.DocumentLoader(t),
},
args: args{
getClaims: func() interface{} {
return testClaims
},
getSignerData: func() *vc.Signer {
s := getTestLDPSigner()
s.KMS = &mockVCSKeyManager{
err: errors.New("some error"),
}

return s
},
},
want: "",
wantErr: true,
},
{
name: "Error NewSigned",
fields: fields{
vdr: &vdrmock.VDRegistry{ResolveValue: createDIDDoc("did:trustbloc:abc")},
documentLoader: testutil.DocumentLoader(t),
},
args: args{
getClaims: func() interface{} {
return func() {}
},
getSignerData: getTestLDPSigner,
},
want: "",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &Crypto{
vdr: tt.fields.vdr,
documentLoader: tt.fields.documentLoader,
}
got, err := c.NewJWTSigned(tt.args.getClaims(), tt.args.getSignerData())
if (err != nil) != tt.wantErr {
t.Errorf("NewJWTSigned() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("NewJWTSigned() got = %v, want %v", got, tt.want)
}
})
}
}
1 change: 1 addition & 0 deletions pkg/profile/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ type OIDCConfig struct {
InitialAccessTokenLifespan time.Duration `json:"initial_access_token_lifespan"`
PreAuthorizedGrantAnonymousAccessSupported bool `json:"pre-authorized_grant_anonymous_access_supported"`
WalletInitiatedAuthFlowSupported bool `json:"wallet_initiated_auth_flow_supported"`
SignedCredentialOfferSupported bool `json:"signed_credential_offer_supported"`
ClaimsEndpoint string `json:"claims_endpoint"`
}

Expand Down
Loading

0 comments on commit 9833912

Please sign in to comment.