247 lines
8.2 KiB
Go
247 lines
8.2 KiB
Go
package vc
|
|
|
|
import (
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/beclab/Olares/cli/pkg/web5/dids/did"
|
|
"github.com/beclab/Olares/cli/pkg/web5/jwt"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// these constants are defined in the W3C Verifiable Credential Data Model specification for:
|
|
// - [Context]
|
|
// - [Type]
|
|
//
|
|
// [Context]: https://www.w3.org/TR/vc-data-model/#contexts
|
|
// [Type]: https://www.w3.org/TR/vc-data-model/#dfn-type
|
|
const (
|
|
BaseContext = "https://www.w3.org/2018/credentials/v1"
|
|
BaseType = "VerifiableCredential"
|
|
)
|
|
|
|
// DataModel represents the W3C Verifiable Credential Data Model defined [here]
|
|
//
|
|
// [here]: https://www.w3.org/TR/vc-data-model/
|
|
type DataModel[T CredentialSubject] struct {
|
|
Context []string `json:"@context"` // https://www.w3.org/TR/vc-data-model/#contexts
|
|
Type []string `json:"type"` // https://www.w3.org/TR/vc-data-model/#dfn-type
|
|
Issuer string `json:"issuer"` // https://www.w3.org/TR/vc-data-model/#issuer
|
|
CredentialSubject T `json:"credentialSubject"` // https://www.w3.org/TR/vc-data-model/#credential-subject
|
|
ID string `json:"id,omitempty"` // https://www.w3.org/TR/vc-data-model/#identifiers
|
|
IssuanceDate string `json:"issuanceDate"` // https://www.w3.org/TR/vc-data-model/#issuance-date
|
|
ExpirationDate string `json:"expirationDate,omitempty"` // https://www.w3.org/TR/vc-data-model/#expiration
|
|
CredentialSchema []CredentialSchema `json:"credentialSchema,omitempty"` // https://www.w3.org/TR/vc-data-model-2.0/#data-schemas
|
|
Evidence []Evidence `json:"evidence,omitempty"` // https://www.w3.org/TR/vc-data-model/#evidence
|
|
}
|
|
|
|
// Evidence represents the evidence property of a Verifiable Credential.
|
|
type Evidence struct {
|
|
ID string `json:"id,omitempty"`
|
|
Type string `json:"type,omitempty"`
|
|
// todo is `AdditionalFields` the right name?
|
|
AdditionalFields map[string]interface{}
|
|
}
|
|
|
|
// CredentialSubject is implemented by any type that can be used as the CredentialSubject
|
|
// of a Verifiable Credential.
|
|
//
|
|
// # Note
|
|
//
|
|
// The VC Data Model specification states that id is not a required field for [CredentialSubject]. However,
|
|
// we've chosen to require it in order to necessitate that all credential's include a subject as we were unable
|
|
// to find a use case where a credential would not be issued to a single subject. Further, the spec states that
|
|
// [vc-jwt] requires the sub be set to the id of the CredentialSubject which becomes difficult to assert while
|
|
// also providing the ability to leverage strongly typed claims.
|
|
//
|
|
// [CredentialSubject]: https://www.w3.org/TR/vc-data-model/#credential-subject
|
|
// [vc-jwt]: https://www.w3.org/TR/vc-data-model/#json-web-token
|
|
type CredentialSubject interface {
|
|
GetID() string
|
|
SetID(id string)
|
|
}
|
|
|
|
// CredentialSchema represents the credentialSchema property of a Verifiable Credential.
|
|
// more information can be found [here]
|
|
//
|
|
// [here]: https://www.w3.org/TR/vc-data-model-2.0/#data-schemas
|
|
type CredentialSchema struct {
|
|
Type string `json:"type"`
|
|
ID string `json:"id"`
|
|
}
|
|
|
|
// Claims is a type alias for a map[string]any that can be used to represent the claims of a Verifiable Credential
|
|
// when the structure of the claims is not known at compile time.
|
|
type Claims map[string]any
|
|
|
|
// GetID returns the id of the CredentialSubject. used to set the sub claim of a vc-jwt in [vcjwt.Sign]
|
|
func (c Claims) GetID() string {
|
|
id, _ := c["id"].(string)
|
|
return id
|
|
}
|
|
|
|
// SetID sets the id of the CredentialSubject. used to set the sub claim of a vc-jwt in [vcjwt.Verify]
|
|
func (c Claims) SetID(id string) {
|
|
c["id"] = id
|
|
}
|
|
|
|
// createOptions contains all of the options that can be passed to [Create]
|
|
type createOptions struct {
|
|
contexts []string
|
|
types []string
|
|
id string
|
|
issuanceDate time.Time
|
|
expirationDate time.Time
|
|
schemas []CredentialSchema
|
|
evidence []Evidence
|
|
}
|
|
|
|
// CreateOption is the return type of all Option functions that can be passed to [Create]
|
|
type CreateOption func(*createOptions)
|
|
|
|
// Contexts can be used to add additional contexts to the Verifiable Credential created by [Create]
|
|
func Contexts(contexts ...string) CreateOption {
|
|
return func(o *createOptions) {
|
|
if o.contexts != nil {
|
|
o.contexts = append(o.contexts, contexts...)
|
|
} else {
|
|
o.contexts = contexts
|
|
}
|
|
}
|
|
}
|
|
|
|
// Schemas can be used to include JSON Schemas within the Verifiable Credential created by [Create]
|
|
// more information can be found [here]
|
|
//
|
|
// [here]: https://www.w3.org/TR/vc-data-model-2.0/#data-schemas
|
|
func Schemas(schemas ...string) CreateOption {
|
|
return func(o *createOptions) {
|
|
if o.schemas != nil {
|
|
o.schemas = make([]CredentialSchema, 0, len(schemas))
|
|
}
|
|
|
|
for _, schema := range schemas {
|
|
o.schemas = append(o.schemas, CredentialSchema{Type: "JsonSchema", ID: schema})
|
|
}
|
|
}
|
|
}
|
|
|
|
// Types can be used to add additional types to the Verifiable Credential created by [Create]
|
|
func Types(types ...string) CreateOption {
|
|
return func(o *createOptions) {
|
|
if o.types != nil {
|
|
o.types = append(o.types, types...)
|
|
} else {
|
|
o.types = types
|
|
}
|
|
}
|
|
}
|
|
|
|
// ID can be used to override the default ID generated by [Create]
|
|
func ID(id string) CreateOption {
|
|
return func(o *createOptions) {
|
|
o.id = id
|
|
}
|
|
}
|
|
|
|
// IssuanceDate can be used to override the default issuance date generated by [Create]
|
|
func IssuanceDate(issuanceDate time.Time) CreateOption {
|
|
return func(o *createOptions) {
|
|
o.issuanceDate = issuanceDate
|
|
}
|
|
}
|
|
|
|
// ExpirationDate can be used to set the expiration date of the Verifiable Credential created by [Create]
|
|
func ExpirationDate(expirationDate time.Time) CreateOption {
|
|
return func(o *createOptions) {
|
|
o.expirationDate = expirationDate
|
|
}
|
|
}
|
|
|
|
// Evidences can be used to set the evidence array of the Verifiable Credential created by [Create]
|
|
func Evidences(evidence ...Evidence) CreateOption {
|
|
return func(o *createOptions) {
|
|
o.evidence = evidence
|
|
}
|
|
}
|
|
|
|
// Create returns a new Verifiable Credential with the provided claims and options.
|
|
// if no options are provided, the following defaults will be used:
|
|
// - ID: urn:vc:uuid:<uuid>
|
|
// - Contexts: ["https://www.w3.org/2018/credentials/v1"]
|
|
// - Types: ["VerifiableCredential"]
|
|
// - IssuanceDate: time.Now()
|
|
//
|
|
// # Note
|
|
//
|
|
// Any additional contexts or types provided will be appended to the defaults in order to remain conformant with
|
|
// the W3C Verifiable Credential Data Model specification
|
|
func Create[T CredentialSubject](claims T, opts ...CreateOption) DataModel[T] {
|
|
o := createOptions{
|
|
id: "urn:vc:uuid:" + uuid.New().String(),
|
|
contexts: []string{BaseContext},
|
|
types: []string{BaseType},
|
|
issuanceDate: time.Now(),
|
|
}
|
|
|
|
for _, f := range opts {
|
|
f(&o)
|
|
}
|
|
|
|
cred := DataModel[T]{
|
|
Context: o.contexts,
|
|
Type: o.types,
|
|
ID: o.id,
|
|
IssuanceDate: o.issuanceDate.UTC().Format(time.RFC3339),
|
|
CredentialSubject: claims,
|
|
Evidence: o.evidence,
|
|
}
|
|
|
|
if len(o.schemas) > 0 {
|
|
cred.CredentialSchema = o.schemas
|
|
}
|
|
|
|
if (o.expirationDate != time.Time{}) {
|
|
cred.ExpirationDate = o.expirationDate.UTC().Format(time.RFC3339)
|
|
}
|
|
|
|
return cred
|
|
}
|
|
|
|
// Sign returns a signed JWT conformant with the [vc-jwt] format. sets the provided vc as value of
|
|
// the "vc" claim in the jwt. It returns the signed jwt and an error if the signing fails.
|
|
//
|
|
// [vc-jwt]: https://www.w3.org/TR/vc-data-model/#json-web-token
|
|
func (vc DataModel[T]) Sign(bearerDID did.BearerDID, opts ...jwt.SignOpt) (string, error) {
|
|
vc.Issuer = bearerDID.URI
|
|
jwtClaims := jwt.Claims{
|
|
Issuer: vc.Issuer,
|
|
JTI: vc.ID,
|
|
Subject: vc.CredentialSubject.GetID(),
|
|
}
|
|
|
|
t, err := time.Parse(time.RFC3339, vc.IssuanceDate)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to parse issuance date: %w", err)
|
|
}
|
|
|
|
jwtClaims.NotBefore = t.Unix()
|
|
|
|
if vc.ExpirationDate != "" {
|
|
t, err := time.Parse(time.RFC3339, vc.ExpirationDate)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to parse expiration date: %w", err)
|
|
}
|
|
|
|
jwtClaims.Expiration = t.Unix()
|
|
}
|
|
|
|
jwtClaims.Misc = make(map[string]any)
|
|
jwtClaims.Misc["vc"] = vc
|
|
|
|
// typ must be set to "JWT" as per the spec
|
|
opts = append(opts, jwt.Type("JWT"))
|
|
return jwt.Sign(jwtClaims, bearerDID, opts...)
|
|
}
|