|
| 1 | +package common |
| 2 | + |
| 3 | +import ( |
| 4 | + "encoding/json" |
| 5 | + "time" |
| 6 | +) |
| 7 | + |
| 8 | +// W3C Verifiable Credentials Data Model constants |
| 9 | +// See https://www.w3.org/TR/vc-data-model-2.0/ |
| 10 | +const ( |
| 11 | + // ContextCredentialsV1 is the W3C VC Data Model v1.1 context URI. |
| 12 | + ContextCredentialsV1 = "https://www.w3.org/2018/credentials/v1" |
| 13 | + |
| 14 | + // ContextCredentialsV2 is the W3C VC Data Model v2.0 context URI. |
| 15 | + ContextCredentialsV2 = "https://www.w3.org/ns/credentials/v2" |
| 16 | + |
| 17 | + // TypeVerifiableCredential is the base type for all Verifiable Credentials. |
| 18 | + TypeVerifiableCredential = "VerifiableCredential" |
| 19 | + |
| 20 | + // TypeVerifiablePresentation is the base type for all Verifiable Presentations. |
| 21 | + TypeVerifiablePresentation = "VerifiablePresentation" |
| 22 | + |
| 23 | + // JSONLDKeyContext is the JSON-LD @context key. |
| 24 | + JSONLDKeyContext = "@context" |
| 25 | + |
| 26 | + // JSONLDKeyType is the JSON-LD type key. |
| 27 | + JSONLDKeyType = "type" |
| 28 | + |
| 29 | + // JSONLDKeyID is the JSON-LD id key. |
| 30 | + JSONLDKeyID = "id" |
| 31 | + |
| 32 | + // VCKeyIssuer is the issuer key in a VC JSON representation. |
| 33 | + VCKeyIssuer = "issuer" |
| 34 | + |
| 35 | + // VCKeyCredentialSubject is the credentialSubject key in a VC JSON representation. |
| 36 | + VCKeyCredentialSubject = "credentialSubject" |
| 37 | + |
| 38 | + // VCKeyValidFrom is the validFrom key (VC Data Model 2.0). |
| 39 | + VCKeyValidFrom = "validFrom" |
| 40 | + |
| 41 | + // VCKeyValidUntil is the validUntil key (VC Data Model 2.0). |
| 42 | + VCKeyValidUntil = "validUntil" |
| 43 | + |
| 44 | + // VCKeyCredentialStatus is the credentialStatus key. |
| 45 | + VCKeyCredentialStatus = "credentialStatus" |
| 46 | + |
| 47 | + // VCKeyCredentialSchema is the credentialSchema key. |
| 48 | + VCKeyCredentialSchema = "credentialSchema" |
| 49 | + |
| 50 | + // VCKeyEvidence is the evidence key. |
| 51 | + VCKeyEvidence = "evidence" |
| 52 | + |
| 53 | + // VCKeyTermsOfUse is the termsOfUse key. |
| 54 | + VCKeyTermsOfUse = "termsOfUse" |
| 55 | + |
| 56 | + // VCKeyRefreshService is the refreshService key. |
| 57 | + VCKeyRefreshService = "refreshService" |
| 58 | + |
| 59 | + // VPKeyHolder is the holder key in a VP JSON representation. |
| 60 | + VPKeyHolder = "holder" |
| 61 | + |
| 62 | + // VPKeyVerifiableCredential is the verifiableCredential key in a VP JSON representation. |
| 63 | + VPKeyVerifiableCredential = "verifiableCredential" |
| 64 | + |
| 65 | + // VPKeyProof is the proof key in a VP/VC JSON representation. |
| 66 | + VPKeyProof = "proof" |
| 67 | + |
| 68 | + // VCKeyIssuanceDate is the issuanceDate key (VC Data Model 1.1). |
| 69 | + VCKeyIssuanceDate = "issuanceDate" |
| 70 | + |
| 71 | + // VCKeyExpirationDate is the expirationDate key (VC Data Model 1.1). |
| 72 | + VCKeyExpirationDate = "expirationDate" |
| 73 | + |
| 74 | + // VCKeyIssued is the issued key (legacy VC date field). |
| 75 | + VCKeyIssued = "issued" |
| 76 | +) |
| 77 | + |
| 78 | +// JWT standard claim keys (RFC 7519). |
| 79 | +const ( |
| 80 | + JWTClaimIss = "iss" // Issuer |
| 81 | + JWTClaimSub = "sub" // Subject |
| 82 | + JWTClaimJti = "jti" // JWT ID |
| 83 | + JWTClaimNbf = "nbf" // Not Before |
| 84 | + JWTClaimIat = "iat" // Issued At |
| 85 | + JWTClaimExp = "exp" // Expiration Time |
| 86 | +) |
| 87 | + |
| 88 | +// JWT-VC/VP specific claim keys. |
| 89 | +const ( |
| 90 | + JWTClaimVC = "vc" // VC claim in a JWT-encoded Verifiable Credential |
| 91 | + JWTClaimVP = "vp" // VP claim in a JWT-encoded Verifiable Presentation |
| 92 | + JWTClaimVct = "vct" // Verifiable Credential Type (SD-JWT VC) |
| 93 | + JWTClaimCnf = "cnf" // Confirmation method (RFC 7800, used for cryptographic holder binding) |
| 94 | + CnfKeyJWK = "jwk" // JWK key within the cnf claim (RFC 7800 §3.2) |
| 95 | +) |
| 96 | + |
| 97 | +// JSONObject is an alias for a generic JSON map. |
| 98 | +type JSONObject = map[string]interface{} |
| 99 | + |
| 100 | +// CustomFields holds additional fields beyond the standard VC fields. |
| 101 | +type CustomFields map[string]interface{} |
| 102 | + |
| 103 | +// Issuer identifies the entity that issued a Verifiable Credential. |
| 104 | +type Issuer struct { |
| 105 | + ID string |
| 106 | +} |
| 107 | + |
| 108 | +// Subject holds the claims made about a credential subject. |
| 109 | +type Subject struct { |
| 110 | + ID string |
| 111 | + CustomFields map[string]interface{} |
| 112 | +} |
| 113 | + |
| 114 | +// TypedID represents a typed identifier used for status, schema, evidence, etc. |
| 115 | +type TypedID struct { |
| 116 | + ID string |
| 117 | + Type string |
| 118 | +} |
| 119 | + |
| 120 | +// CredentialContents contains the structured content of a Verifiable Credential. |
| 121 | +// Fields align with the W3C VC Data Model 2.0 specification. |
| 122 | +type CredentialContents struct { |
| 123 | + Context []string |
| 124 | + ID string |
| 125 | + Types []string |
| 126 | + Issuer *Issuer |
| 127 | + Subject []Subject |
| 128 | + ValidFrom *time.Time |
| 129 | + ValidUntil *time.Time |
| 130 | + Status *TypedID |
| 131 | + Schemas []TypedID |
| 132 | + Evidence []interface{} |
| 133 | + TermsOfUse []TypedID |
| 134 | + RefreshService []TypedID |
| 135 | +} |
| 136 | + |
| 137 | +// Credential represents a Verifiable Credential. |
| 138 | +type Credential struct { |
| 139 | + contents CredentialContents |
| 140 | + customFields CustomFields |
| 141 | + // rawJSON, if set, is returned by ToRawJSON() instead of building from contents. |
| 142 | + rawJSON JSONObject |
| 143 | +} |
| 144 | + |
| 145 | +// Contents returns the structured content of the credential. |
| 146 | +func (c *Credential) Contents() CredentialContents { |
| 147 | + return c.contents |
| 148 | +} |
| 149 | + |
| 150 | +// CustomFields returns the custom fields of the credential. |
| 151 | +func (c *Credential) CustomFields() CustomFields { |
| 152 | + return c.customFields |
| 153 | +} |
| 154 | + |
| 155 | +// ToRawJSON converts the credential to a JSON map representation. |
| 156 | +// Custom fields from the subject are placed at the top level of credentialSubject. |
| 157 | +func (c *Credential) ToRawJSON() JSONObject { |
| 158 | + if c.rawJSON != nil { |
| 159 | + return c.rawJSON |
| 160 | + } |
| 161 | + result := JSONObject{} |
| 162 | + |
| 163 | + if len(c.contents.Context) > 0 { |
| 164 | + result[JSONLDKeyContext] = c.contents.Context |
| 165 | + } |
| 166 | + if c.contents.ID != "" { |
| 167 | + result[JSONLDKeyID] = c.contents.ID |
| 168 | + } |
| 169 | + if len(c.contents.Types) > 0 { |
| 170 | + result[JSONLDKeyType] = c.contents.Types |
| 171 | + } |
| 172 | + if c.contents.Issuer != nil { |
| 173 | + result[VCKeyIssuer] = c.contents.Issuer.ID |
| 174 | + } |
| 175 | + if c.contents.ValidFrom != nil { |
| 176 | + result[VCKeyValidFrom] = c.contents.ValidFrom.Format(time.RFC3339) |
| 177 | + } |
| 178 | + if c.contents.ValidUntil != nil { |
| 179 | + result[VCKeyValidUntil] = c.contents.ValidUntil.Format(time.RFC3339) |
| 180 | + } |
| 181 | + if c.contents.Status != nil { |
| 182 | + result[VCKeyCredentialStatus] = JSONObject{JSONLDKeyID: c.contents.Status.ID, JSONLDKeyType: c.contents.Status.Type} |
| 183 | + } |
| 184 | + if len(c.contents.Schemas) > 0 { |
| 185 | + result[VCKeyCredentialSchema] = typedIDsToJSON(c.contents.Schemas) |
| 186 | + } |
| 187 | + if len(c.contents.Evidence) > 0 { |
| 188 | + result[VCKeyEvidence] = c.contents.Evidence |
| 189 | + } |
| 190 | + if len(c.contents.TermsOfUse) > 0 { |
| 191 | + result[VCKeyTermsOfUse] = typedIDsToJSON(c.contents.TermsOfUse) |
| 192 | + } |
| 193 | + if len(c.contents.RefreshService) > 0 { |
| 194 | + result[VCKeyRefreshService] = typedIDsToJSON(c.contents.RefreshService) |
| 195 | + } |
| 196 | + |
| 197 | + if len(c.contents.Subject) > 0 { |
| 198 | + subjects := make([]JSONObject, 0, len(c.contents.Subject)) |
| 199 | + for _, s := range c.contents.Subject { |
| 200 | + subj := JSONObject{} |
| 201 | + if s.ID != "" { |
| 202 | + subj[JSONLDKeyID] = s.ID |
| 203 | + } |
| 204 | + for k, v := range s.CustomFields { |
| 205 | + subj[k] = v |
| 206 | + } |
| 207 | + subjects = append(subjects, subj) |
| 208 | + } |
| 209 | + if len(subjects) == 1 { |
| 210 | + result[VCKeyCredentialSubject] = subjects[0] |
| 211 | + } else { |
| 212 | + result[VCKeyCredentialSubject] = subjects |
| 213 | + } |
| 214 | + } |
| 215 | + |
| 216 | + for k, v := range c.customFields { |
| 217 | + if _, exists := result[k]; !exists { |
| 218 | + result[k] = v |
| 219 | + } |
| 220 | + } |
| 221 | + |
| 222 | + return result |
| 223 | +} |
| 224 | + |
| 225 | +// MarshalJSON serializes the credential to JSON bytes. |
| 226 | +func (c *Credential) MarshalJSON() ([]byte, error) { |
| 227 | + return json.Marshal(c.ToRawJSON()) |
| 228 | +} |
| 229 | + |
| 230 | +// SetRawJSON stores a pre-built raw JSON map to be returned by ToRawJSON(). |
| 231 | +func (c *Credential) SetRawJSON(raw JSONObject) { |
| 232 | + c.rawJSON = raw |
| 233 | +} |
| 234 | + |
| 235 | +// CreateCredential constructs a Credential from CredentialContents and custom fields. |
| 236 | +func CreateCredential(contents CredentialContents, customFields CustomFields) (*Credential, error) { |
| 237 | + return &Credential{ |
| 238 | + contents: contents, |
| 239 | + customFields: customFields, |
| 240 | + }, nil |
| 241 | +} |
| 242 | + |
| 243 | +// PresentationOpt is a functional option for configuring a Presentation. |
| 244 | +type PresentationOpt func(*Presentation) |
| 245 | + |
| 246 | +// Presentation represents a Verifiable Presentation. |
| 247 | +type Presentation struct { |
| 248 | + Context []string |
| 249 | + ID string |
| 250 | + Type []string |
| 251 | + Holder string |
| 252 | + credentials []*Credential |
| 253 | + Proof *LDProof |
| 254 | + // holderKey stores the resolved public key that signed the VP JWT. |
| 255 | + // Stored as interface{} to avoid jwx dependency in the common package. |
| 256 | + // The verifier package type-asserts to jwk.Key. |
| 257 | + holderKey interface{} |
| 258 | +} |
| 259 | + |
| 260 | +// HolderKey returns the public key that signed the VP JWT, if available. |
| 261 | +func (p *Presentation) HolderKey() interface{} { |
| 262 | + return p.holderKey |
| 263 | +} |
| 264 | + |
| 265 | +// SetHolderKey stores the public key that signed the VP JWT. |
| 266 | +func (p *Presentation) SetHolderKey(key interface{}) { |
| 267 | + p.holderKey = key |
| 268 | +} |
| 269 | + |
| 270 | +// Credentials returns the credentials contained in the presentation. |
| 271 | +func (p *Presentation) Credentials() []*Credential { |
| 272 | + return p.credentials |
| 273 | +} |
| 274 | + |
| 275 | +// AddCredentials appends one or more credentials to the presentation. |
| 276 | +func (p *Presentation) AddCredentials(credentials ...*Credential) { |
| 277 | + p.credentials = append(p.credentials, credentials...) |
| 278 | +} |
| 279 | + |
| 280 | +// MarshalJSON serializes the presentation to JSON bytes. |
| 281 | +func (p *Presentation) MarshalJSON() ([]byte, error) { |
| 282 | + result := JSONObject{} |
| 283 | + |
| 284 | + ctx := p.Context |
| 285 | + if len(ctx) == 0 { |
| 286 | + ctx = []string{ContextCredentialsV1} |
| 287 | + } |
| 288 | + result[JSONLDKeyContext] = ctx |
| 289 | + |
| 290 | + types := p.Type |
| 291 | + if len(types) == 0 { |
| 292 | + types = []string{TypeVerifiablePresentation} |
| 293 | + } |
| 294 | + result[JSONLDKeyType] = types |
| 295 | + |
| 296 | + if p.ID != "" { |
| 297 | + result[JSONLDKeyID] = p.ID |
| 298 | + } |
| 299 | + if p.Holder != "" { |
| 300 | + result[VPKeyHolder] = p.Holder |
| 301 | + } |
| 302 | + |
| 303 | + if len(p.credentials) > 0 { |
| 304 | + vcs := make([]json.RawMessage, 0, len(p.credentials)) |
| 305 | + for _, cred := range p.credentials { |
| 306 | + credJSON, err := cred.MarshalJSON() |
| 307 | + if err != nil { |
| 308 | + return nil, err |
| 309 | + } |
| 310 | + vcs = append(vcs, credJSON) |
| 311 | + } |
| 312 | + result[VPKeyVerifiableCredential] = vcs |
| 313 | + } |
| 314 | + |
| 315 | + if p.Proof != nil { |
| 316 | + result[VPKeyProof] = p.Proof |
| 317 | + } |
| 318 | + |
| 319 | + return json.Marshal(result) |
| 320 | +} |
| 321 | + |
| 322 | +// NewPresentation creates a new Presentation with the given options applied. |
| 323 | +func NewPresentation(opts ...PresentationOpt) (*Presentation, error) { |
| 324 | + p := &Presentation{} |
| 325 | + for _, opt := range opts { |
| 326 | + opt(p) |
| 327 | + } |
| 328 | + return p, nil |
| 329 | +} |
| 330 | + |
| 331 | +// WithCredentials returns a PresentationOpt that adds credentials to a presentation. |
| 332 | +func WithCredentials(credentials ...*Credential) PresentationOpt { |
| 333 | + return func(p *Presentation) { |
| 334 | + p.AddCredentials(credentials...) |
| 335 | + } |
| 336 | +} |
| 337 | + |
| 338 | +// typedIDsToJSON converts a slice of TypedID to JSON-compatible format. |
| 339 | +func typedIDsToJSON(ids []TypedID) []JSONObject { |
| 340 | + result := make([]JSONObject, 0, len(ids)) |
| 341 | + for _, id := range ids { |
| 342 | + obj := JSONObject{} |
| 343 | + if id.ID != "" { |
| 344 | + obj[JSONLDKeyID] = id.ID |
| 345 | + } |
| 346 | + if id.Type != "" { |
| 347 | + obj[JSONLDKeyType] = id.Type |
| 348 | + } |
| 349 | + result = append(result, obj) |
| 350 | + } |
| 351 | + return result |
| 352 | +} |
0 commit comments