diff --git a/TODO.md b/TODO.md index 2251498..687929a 100644 --- a/TODO.md +++ b/TODO.md @@ -7,6 +7,7 @@ - Option to provide "additional validation" for sd-jwt validation - Option to provide "additional validation" for kb-jwt validation - Function to retrieve kb-jwt contents as map +- KB JWT hash validation Signing: diff --git a/disclosure/disclosure.go b/disclosure/disclosure.go new file mode 100644 index 0000000..e897463 --- /dev/null +++ b/disclosure/disclosure.go @@ -0,0 +1,130 @@ +package disclosure + +import ( + "encoding/base64" + "encoding/json" + "fmt" + e "github.com/MichaelFraser99/go-sd-jwt/internal/error" + s "github.com/MichaelFraser99/go-sd-jwt/internal/salt" + "hash" +) + +// Disclosure this object represents a single disclosure in a SD-JWT. +// Salt the base64url encoded cryptographically secure string used during generation +// Key the key of the disclosed value. Only present for disclosed object values and not set when for an array element +// Value the value being disclosed +// EncodedValue the resulting base64url encoded disclosure array +type Disclosure struct { + Salt string + Key *string + Value any + EncodedValue string +} + +func (d *Disclosure) Hash(hash hash.Hash) []byte { + hash.Write([]byte(d.EncodedValue)) + hashedBytes := hash.Sum(nil) + + b64Hash := make([]byte, base64.RawURLEncoding.EncodedLen(len(hashedBytes))) + base64.RawURLEncoding.Encode(b64Hash, hashedBytes) + return b64Hash +} + +func NewFromObject(key string, value any, salt *string) (*Disclosure, error) { + if key == "" || key == "_sd" || key == "..." { + return nil, fmt.Errorf("%winvalid key value provided, must not be empty, '_sd', or '...'", e.InvalidDisclosure) + } + + var saltValue string + if salt == nil { + newSalt, err := s.NewSalt() + if err != nil { + return nil, err + } + saltValue = *newSalt + } else { + saltValue = *salt + } + + disclosureArray := []any{saltValue, key, value} + dBytes, err := json.Marshal(disclosureArray) + if err != nil { + return nil, fmt.Errorf("error encoding disclosure array as bytes: %w", err) + } + + encodedDisclosureArray := make([]byte, base64.RawURLEncoding.EncodedLen(len(dBytes))) + base64.RawURLEncoding.Encode(encodedDisclosureArray, dBytes) + + disclosure := &Disclosure{ + Salt: saltValue, + Key: &key, + Value: value, + EncodedValue: string(encodedDisclosureArray), + } + + return disclosure, nil +} + +func NewFromArrayElement(element any, salt *string) (*Disclosure, error) { + var saltValue string + if salt == nil { + newSalt, err := s.NewSalt() + if err != nil { + return nil, err + } + saltValue = *newSalt + } else { + saltValue = *salt + } + + disclosureArray := []any{saltValue, element} + dBytes, err := json.Marshal(disclosureArray) + if err != nil { + return nil, fmt.Errorf("error encoding disclosure array as bytes: %w", err) + } + + encodedDisclosureArray := make([]byte, base64.RawURLEncoding.EncodedLen(len(dBytes))) + base64.RawURLEncoding.Encode(encodedDisclosureArray, dBytes) + + disclosure := &Disclosure{ + Salt: saltValue, + Value: element, + EncodedValue: string(encodedDisclosureArray), + } + + return disclosure, nil +} + +func NewFromDisclosure(disclosure string) (*Disclosure, error) { + d := &Disclosure{ + EncodedValue: disclosure, + } + + decoded, err := base64.RawURLEncoding.DecodeString(disclosure) + if err != nil { + return nil, fmt.Errorf("%werror base64url decoding provided disclosure: %s", e.InvalidDisclosure, err.Error()) + } + + var dArray []any + err = json.Unmarshal(decoded, &dArray) + if err != nil { + return nil, fmt.Errorf("%werror parsing decoded disclosure as array: %s", e.InvalidDisclosure, err.Error()) + } + + if len(dArray) == 2 { + d.Salt = dArray[0].(string) + d.Value = dArray[1] + } else if len(dArray) == 3 { + d.Salt = dArray[0].(string) + d.Key = String(dArray[1].(string)) + d.Value = dArray[2] + } else { + return nil, fmt.Errorf("%winvalid disclosure contents: %s", e.InvalidDisclosure, string(decoded)) + } + + return d, nil +} + +func String(s string) *string { + return &s +} diff --git a/disclosure/disclosure_test.go b/disclosure/disclosure_test.go new file mode 100644 index 0000000..0353ab2 --- /dev/null +++ b/disclosure/disclosure_test.go @@ -0,0 +1,140 @@ +package disclosure + +import ( + "crypto/sha256" + "fmt" + "testing" +) + +func TestNewFromObject(t *testing.T) { + disclosure, err := NewFromObject("family_name", "Möbius", String("_26bc4LT-ac6q2KI6cBW5es")) + if err != nil { + t.Fatalf("no error expected: %s", err.Error()) + } + + if disclosure.Key == nil { + t.Fatalf("key should not be nil") + } + if *disclosure.Key != "family_name" { + t.Errorf("key should be family_name is: %s", *disclosure.Key) + } + if disclosure.Salt != "_26bc4LT-ac6q2KI6cBW5es" { + t.Errorf("unexpected salt value returned: %s", disclosure.Salt) + } + strValue, ok := disclosure.Value.(string) + if !ok { + t.Fatalf("Returned value should be a string") + } + if strValue != "Möbius" { + t.Errorf("unexpected disclosure value returned: %s", disclosure.Value) + } + if disclosure.EncodedValue != "WyJfMjZiYzRMVC1hYzZxMktJNmNCVzVlcyIsImZhbWlseV9uYW1lIiwiTcO2Yml1cyJd" { + t.Errorf("unexpected encoded value produced: %s", disclosure.EncodedValue) + } +} + +func TestNewFromArrayElement(t *testing.T) { + disclosure, err := NewFromArrayElement("FR", String("lklxF5jMYlGTPUovMNIvCA")) + if err != nil { + t.Fatalf("no error expected: %s", err.Error()) + } + + if disclosure.Key != nil { + t.Fatalf("key should not be nil, is: %s", *disclosure.Key) + } + if disclosure.Salt != "lklxF5jMYlGTPUovMNIvCA" { + t.Errorf("unexpected salt value returned: %s", disclosure.Salt) + } + if string(disclosure.Value.(string)) != "FR" { + t.Errorf("unexpected disclosure value returned: %s", disclosure.Value) + } + if disclosure.EncodedValue != "WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwiRlIiXQ" { + t.Errorf("unexpected encoded value produced: %s", disclosure.EncodedValue) + } +} + +func TestNewFromDisclosureObject(t *testing.T) { + disclosures := []string{ + "WwoiXzI2YmM0TFQtYWM2cTJLSTZjQlc1ZXMiLAoiZmFtaWx5X25hbWUiLAoiTcO2Yml1cyIKXQ", + "WyJfMjZiYzRMVC1hYzZxMktJNmNCVzVlcyIsICJmYW1pbHlfbmFtZSIsICJNXHUwMGY2Yml1cyJd", + "WyJfMjZiYzRMVC1hYzZxMktJNmNCVzVlcyIsImZhbWlseV9uYW1lIiwiTcO2Yml1cyJd", + } + + for i, d := range disclosures { + t.Run(fmt.Sprintf("disclosure-%d", i), func(t *testing.T) { + disclosure, err := NewFromDisclosure(d) + if err != nil { + t.Fatalf("no error expected: %s", err.Error()) + } + + if disclosure.Key == nil { + t.Fatalf("key should not be nil") + } + if *disclosure.Key != "family_name" { + t.Errorf("key should be family_name is: %s", *disclosure.Key) + } + if disclosure.Salt != "_26bc4LT-ac6q2KI6cBW5es" { + t.Errorf("unexpected salt value returned: %s", disclosure.Salt) + } + if disclosure.Value.(string) != "Möbius" { + t.Errorf("unexpected disclosure value returned: %s", disclosure.Value) + } + if disclosure.EncodedValue != d { + t.Errorf("unexpected encoded value produced: %s", disclosure.EncodedValue) + } + }) + } +} + +func TestNewFromDisclosureElementObject(t *testing.T) { + disclosures := []string{ + "WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgIkZSIl0", + "WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwiRlIiXQ", + } + + for i, d := range disclosures { + t.Run(fmt.Sprintf("disclosure-%d", i), func(t *testing.T) { + disclosure, err := NewFromDisclosure(d) + if err != nil { + t.Fatalf("no error expected: %s", err.Error()) + } + + if disclosure.Key != nil { + t.Fatalf("key should not be nil, is: %s", *disclosure.Key) + } + if disclosure.Salt != "lklxF5jMYlGTPUovMNIvCA" { + t.Errorf("unexpected salt value returned: %s", disclosure.Salt) + } + if disclosure.Value.(string) != "FR" { + t.Errorf("unexpected disclosure value returned: %s", disclosure.Value) + } + if disclosure.EncodedValue != d { + t.Errorf("unexpected encoded value produced: %s", disclosure.EncodedValue) + } + }) + } +} + +func TestDisclosure_Hash(t *testing.T) { + objectDisclosure, err := NewFromDisclosure("WyI2cU1RdlJMNWhhaiIsICJmYW1pbHlfbmFtZSIsICJNw7ZiaXVzIl0") + if err != nil { + t.Fatalf("no error expected: %s", err.Error()) + } + + arrayElementDisclosure, err := NewFromDisclosure("WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgIkZSIl0") + if err != nil { + t.Fatalf("no error expected: %s", err.Error()) + } + + hash := sha256.New() + objectHash := objectDisclosure.Hash(hash) + if string(objectHash) != "uutlBuYeMDyjLLTpf6Jxi7yNkEF35jdyWMn9U7b_RYY" { + t.Errorf("unexpected hash result: %s", string(objectHash)) + } + + hash.Reset() + arrayHash := arrayElementDisclosure.Hash(hash) + if string(arrayHash) != "w0I8EKcdCtUPkGCNUrfwVp2xEgNjtoIDlOxc9-PlOhs" { + t.Errorf("unexpected hash result: %s", string(arrayHash)) + } +} diff --git a/e2e/e2e_test.go b/e2e/e2e_test.go new file mode 100644 index 0000000..ce9ba88 --- /dev/null +++ b/e2e/e2e_test.go @@ -0,0 +1,412 @@ +package e2e_test + +import ( + "crypto/rand" + "crypto/sha256" + "encoding/base64" + "encoding/json" + "fmt" + "github.com/MichaelFraser99/go-jose" + "github.com/MichaelFraser99/go-jose/model" + go_sd_jwt "github.com/MichaelFraser99/go-sd-jwt" + "github.com/MichaelFraser99/go-sd-jwt/disclosure" + "runtime" + "testing" +) + +func TestE2E(t *testing.T) { + issuerSigner, err := jose.GetSigner(model.RS256, &model.Opts{BitSize: 4096}) + if err != nil { + t.Fatalf("error creating issuer signer: %s", err.Error()) + } + issuerValidator, err := jose.GetValidator(issuerSigner.Alg(), issuerSigner.Public()) + if err != nil { + t.Fatalf("error creating issuer validator: %s", err.Error()) + } + + holderSigner, err := jose.GetSigner(model.RS256, &model.Opts{BitSize: 4096}) + if err != nil { + t.Fatalf("error creating holder signer: %s", err.Error()) + } + holderValidator, err := jose.GetValidator(holderSigner.Alg(), holderSigner.Public()) + if err != nil { + t.Fatalf("error creating holder validator: %s", err.Error()) + } + + inputData := map[string]any{ + "verified_claims": map[string]any{ + "verification": map[string]any{ + "trust_framework": "de_aml", + "time": "2012-04-23T18:25Z", + "verification_process": "f24c6f-6d3f-4ec5-973e-b0d8506f3bc7", + "evidence": []map[string]any{ + { + "type": "document", + "method": "pipp", + "time": "2012-04-22T11:30Z", + "document": map[string]any{ + "type": "idcard", + "issuer": map[string]any{ + "name": "Stadt Augsburg", + "country": "DE", + }, + "number": "53554554", + "date_of_issuance": "2010-03-23", + "date_of_expiry": "2020-03-22", + }, + }, + }, + }, + "claims": map[string]any{ + "given_name": "Max", + "family_name": "Müller", + "nationalities": []any{"DE"}, + "birthdate": "1956-01-28", + "place_of_birth": map[string]any{ + "country": "IS", + "locality": "Þykkvabæjarklaustur", + }, + "address": map[string]any{ + "locality": "Maxstadt", + "postal_code": "12344", + "country": "DE", + "street_address": "Weidenstraße 22", + }, + }, + }, + "birth_middle_name": "Timotheus", + "salutation": "Dr.", + "msisdn": "49123456789", + } + var sdJwtString string + + t.Run("we can create an SD Jwt as an issuer", func(t *testing.T) { + inputData["cnf"] = holderValidator.Jwk() + + header := map[string]string{ + "typ": "application/json+sd-jwt", + "alg": issuerSigner.Alg().String(), + } + + // Create issuer disclosure + issuerDisclosure, err := disclosure.NewFromObject("issuer", inputData["verified_claims"].(map[string]any)["verification"].(map[string]any)["evidence"].([]map[string]any)[0]["document"].(map[string]any)["issuer"], nil) + if err != nil { + t.Fatalf("error creating disclosure from object: %s", err.Error()) + } + delete(inputData["verified_claims"].(map[string]any)["verification"].(map[string]any)["evidence"].([]map[string]any)[0]["document"].(map[string]any), "issuer") + + // Create date of issuance disclosure + dateOfIssuanceDisclosure, err := disclosure.NewFromObject("date_of_issuance", inputData["verified_claims"].(map[string]any)["verification"].(map[string]any)["evidence"].([]map[string]any)[0]["document"].(map[string]any)["date_of_issuance"], nil) + if err != nil { + t.Fatalf("error creating disclosure from object: %s", err.Error()) + } + delete(inputData["verified_claims"].(map[string]any)["verification"].(map[string]any)["evidence"].([]map[string]any)[0]["document"].(map[string]any), "date_of_issuance") + + // Create number disclosure + numberDisclosure, err := disclosure.NewFromObject("number", inputData["verified_claims"].(map[string]any)["verification"].(map[string]any)["evidence"].([]map[string]any)[0]["document"].(map[string]any)["number"], nil) + if err != nil { + t.Fatalf("error creating disclosure from object: %s", err.Error()) + } + delete(inputData["verified_claims"].(map[string]any)["verification"].(map[string]any)["evidence"].([]map[string]any)[0]["document"].(map[string]any), "number") + + // Create nationalities disclosure + nationalitiesDEDisclosure, err := disclosure.NewFromArrayElement(inputData["verified_claims"].(map[string]any)["claims"].(map[string]any)["nationalities"].([]any)[0], nil) + if err != nil { + t.Fatalf("error creating disclosure from array element: %s", err.Error()) + } + inputData["verified_claims"].(map[string]any)["claims"].(map[string]any)["nationalities"].([]any)[0] = map[string]any{"...": string(nationalitiesDEDisclosure.Hash(sha256.New()))} + + // Add disclosures to array + inputData["verified_claims"].(map[string]any)["verification"].(map[string]any)["evidence"].([]map[string]any)[0]["document"].(map[string]any)["_sd"] = []string{ + string(issuerDisclosure.Hash(sha256.New())), + string(dateOfIssuanceDisclosure.Hash(sha256.New())), + string(numberDisclosure.Hash(sha256.New())), + } + + // Create evidence disclosure + evidenceDisclosure, err := disclosure.NewFromObject("evidence", inputData["verified_claims"].(map[string]any)["verification"].(map[string]any)["evidence"], nil) + if err != nil { + t.Fatalf("error creating disclosure from object: %s", err.Error()) + } + delete(inputData["verified_claims"].(map[string]any)["verification"].(map[string]any), "evidence") + + // Add disclosures to array + inputData["verified_claims"].(map[string]any)["verification"].(map[string]any)["_sd"] = []string{ + string(evidenceDisclosure.Hash(sha256.New())), + } + + headerBytes, err := json.Marshal(header) + if err != nil { + t.Fatalf("error marshalling header as bytes: %s", err.Error()) + } + bodyBytes, err := json.Marshal(inputData) + if err != nil { + t.Fatalf("error marshalling body as bytes: %s", err.Error()) + } + + b64Header := make([]byte, base64.RawURLEncoding.EncodedLen(len(headerBytes))) + base64.RawURLEncoding.Encode(b64Header, headerBytes) + b64Body := make([]byte, base64.RawURLEncoding.EncodedLen(len(bodyBytes))) + base64.RawURLEncoding.Encode(b64Body, bodyBytes) + + jwt := fmt.Sprintf("%s.%s", string(b64Header), string(b64Body)) + + signature, err := issuerSigner.Sign(rand.Reader, []byte(jwt), nil) + if err != nil { + t.Fatalf("error when signing provided jwt: %s", err.Error()) + } + b64Signature := make([]byte, base64.RawURLEncoding.EncodedLen(len(signature))) + base64.RawURLEncoding.Encode(b64Signature, signature) + + jwt = fmt.Sprintf("%s.%s", jwt, string(b64Signature)) + sdJwtString = fmt.Sprintf("%s~%s~%s~%s~%s~%s~", jwt, + issuerDisclosure.EncodedValue, + dateOfIssuanceDisclosure.EncodedValue, + numberDisclosure.EncodedValue, + evidenceDisclosure.EncodedValue, + nationalitiesDEDisclosure.EncodedValue, + ) + }) + + jwkBytes, err := json.Marshal(issuerValidator.Jwk()) + if err != nil { + t.Fatalf("error creating jwk from validator") + } + + t.Log(sdJwtString) + t.Log(string(jwkBytes)) + + var sdJwt *go_sd_jwt.SdJwt + var disclosedClaims map[string]any + t.Run("we can create an sd jwt object from the newly created sd jwt string", func(t *testing.T) { + sdJwt, err = go_sd_jwt.New(sdJwtString) + if err != nil { + t.Fatalf("error creating sd jwt object from created sd jwt string: %s", err.Error()) + } + disclosedClaims, err = sdJwt.GetDisclosedClaims() + if err != nil { + t.Fatalf("error disclosing claims: %s", err.Error()) + } + + body := *sdJwt.Body() + + t.Run("validate body", func(t *testing.T) { + keyPresent(t, body, "birth_middle_name") + keyPresent(t, body, "msisdn") + keyPresent(t, body, "salutation") + keyPresent(t, body, "cnf") + keyPresent(t, body, "verified_claims") + }) + + cnf := keyPresent(t, body, "cnf").(map[string]any) + t.Run("validate cnf", func(t *testing.T) { + keyPresent(t, cnf, "e") + keyPresent(t, cnf, "kty") + keyPresent(t, cnf, "n") + }) + + verifiedClaims := keyPresent(t, body, "verified_claims").(map[string]any) + t.Run("validate verified_claims", func(t *testing.T) { + keyPresent(t, verifiedClaims, "claims") + keyPresent(t, verifiedClaims, "verification") + }) + + claims := keyPresent(t, verifiedClaims, "claims").(map[string]any) + t.Run("validate claims", func(t *testing.T) { + keyPresent(t, claims, "address") + keyPresent(t, claims, "birthdate") + keyPresent(t, claims, "family_name") + keyPresent(t, claims, "given_name") + keyPresent(t, claims, "nationalities") + keyPresent(t, claims, "place_of_birth") + }) + + address := keyPresent(t, claims, "address").(map[string]any) + t.Run("validate address", func(t *testing.T) { + keyPresent(t, address, "country") + keyPresent(t, address, "locality") + keyPresent(t, address, "postal_code") + keyPresent(t, address, "street_address") + }) + + nationalities := keyPresent(t, claims, "nationalities").([]any) + t.Run("validate nationalities", func(t *testing.T) { + if len(nationalities) != 1 { + t.Errorf("nationalities has wrong length: %d", len(nationalities)) + } + deMap, ok := nationalities[0].(map[string]any) + if !ok { + t.Error("nationalities should have a single map element") + } + if len(deMap) != 1 { + t.Error("the map should have a single key") + } + _, ok = deMap["..."] + if !ok { + t.Error("the map should have a single value of '...'") + } + }) + + placeOfBirth := keyPresent(t, claims, "place_of_birth").(map[string]any) + t.Run("validate place_of_birth", func(t *testing.T) { + keyPresent(t, placeOfBirth, "country") + keyPresent(t, placeOfBirth, "locality") + }) + + verification := keyPresent(t, verifiedClaims, "verification").(map[string]any) + t.Run("validate verification", func(t *testing.T) { + keyNotPresent(t, verification, "evidence") + keyPresent(t, verification, "time") + keyPresent(t, verification, "trust_framework") + keyPresent(t, verification, "verification_process") + }) + }) + + t.Run("we can validate the disclosed claims from our SdJwt", func(t *testing.T) { + t.Run("validate disclosed claims", func(t *testing.T) { + keyPresent(t, disclosedClaims, "birth_middle_name") + keyPresent(t, disclosedClaims, "msisdn") + keyPresent(t, disclosedClaims, "salutation") + keyPresent(t, disclosedClaims, "cnf") + keyPresent(t, disclosedClaims, "verified_claims") + }) + + cnf := keyPresent(t, disclosedClaims, "cnf").(map[string]any) + t.Run("validate cnf", func(t *testing.T) { + keyPresent(t, cnf, "e") + keyPresent(t, cnf, "kty") + keyPresent(t, cnf, "n") + }) + + verifiedClaims := keyPresent(t, disclosedClaims, "verified_claims").(map[string]any) + t.Run("validate verified_claims", func(t *testing.T) { + keyPresent(t, verifiedClaims, "claims") + keyPresent(t, verifiedClaims, "verification") + }) + + claims := keyPresent(t, verifiedClaims, "claims").(map[string]any) + t.Run("validate claims", func(t *testing.T) { + keyPresent(t, claims, "address") + keyPresent(t, claims, "birthdate") + keyPresent(t, claims, "family_name") + keyPresent(t, claims, "given_name") + keyPresent(t, claims, "nationalities") + keyPresent(t, claims, "place_of_birth") + }) + + address := keyPresent(t, claims, "address").(map[string]any) + t.Run("validate address", func(t *testing.T) { + keyPresent(t, address, "country") + keyPresent(t, address, "locality") + keyPresent(t, address, "postal_code") + keyPresent(t, address, "street_address") + }) + + nationalities := keyPresent(t, claims, "nationalities").([]any) + t.Run("validate nationalities", func(t *testing.T) { + if len(nationalities) != 1 { + t.Errorf("nationalities has wrong length: %d", len(nationalities)) + } + de, ok := nationalities[0].(string) + if !ok { + t.Error("nationalities should have a single string") + } + if de != "DE" { + t.Errorf("incorrect nationalities value storred: %s", de) + } + }) + + placeOfBirth := keyPresent(t, claims, "place_of_birth").(map[string]any) + t.Run("validate place_of_birth", func(t *testing.T) { + keyPresent(t, placeOfBirth, "country") + keyPresent(t, placeOfBirth, "locality") + }) + + verification := keyPresent(t, verifiedClaims, "verification").(map[string]any) + t.Run("validate verification", func(t *testing.T) { + keyPresent(t, verification, "evidence") + keyPresent(t, verification, "time") + keyPresent(t, verification, "trust_framework") + keyPresent(t, verification, "verification_process") + }) + + evidence := keyPresent(t, verification, "evidence").([]any) + t.Run("validate evidence", func(t *testing.T) { + if len(evidence) != 1 { + t.Errorf("evidence has wrong length: %d", len(nationalities)) + } + _, ok := evidence[0].(map[string]any) + if !ok { + t.Error("evidence should have a single map") + } + }) + + evidenceContents := keyPresent(t, verification, "evidence").([]any)[0].(map[string]any) + t.Run("validate evidence contents", func(t *testing.T) { + keyPresent(t, evidenceContents, "document") + keyPresent(t, evidenceContents, "method") + keyPresent(t, evidenceContents, "time") + keyPresent(t, evidenceContents, "type") + }) + + document := keyPresent(t, evidenceContents, "document").(map[string]any) + t.Run("validate document", func(t *testing.T) { + keyNotPresent(t, document, "_sd") + keyPresent(t, document, "date_of_expiry") + keyPresent(t, document, "type") + issuer := keyPresent(t, document, "issuer") + dateOfIssuance := keyPresent(t, document, "date_of_issuance") + number := keyPresent(t, document, "number") + + //Validate issuer + issuerMap, ok := issuer.(map[string]any) + if !ok { + t.Error("disclosed issuer value should be a map") + } + if len(issuerMap) != 2 { + t.Errorf("issuer key is incorrect length: %d", len(issuerMap)) + } + if issuerMap["name"] != "Stadt Augsburg" { + t.Errorf("incorrect name value returned: %s", issuerMap["name"]) + } + if issuerMap["country"] != "DE" { + t.Errorf("incorrect country value returned: %s", issuerMap["country"]) + } + + //Validate date of issuance + dateOfIssuanceString, ok := dateOfIssuance.(string) + if !ok { + t.Error("disclosed date of issuance value should be a string") + } + if dateOfIssuanceString != "2010-03-23" { + t.Errorf("incorrect date of issuance value returned: %s", dateOfIssuanceString) + } + + //Validate number + numberString, ok := number.(string) + if !ok { + t.Error("disclosed number value should be a string") + } + if numberString != "53554554" { + t.Errorf("incorrect number value returned: %s", numberString) + } + }) + }) +} + +func keyPresent(t *testing.T, data map[string]any, key string) any { + val, ok := data[key] + if !ok { + _, file, line, _ := runtime.Caller(1) + t.Errorf("%s should exist\n\t%s:%v", key, file, line) + } + return val +} + +func keyNotPresent(t *testing.T, data map[string]any, key string) { + _, ok := data[key] + if ok { + _, file, line, _ := runtime.Caller(1) + t.Errorf("%s should not exist\n\t%s:%v", key, file, line) + } +} diff --git a/getters.go b/getters.go deleted file mode 100644 index 36696bc..0000000 --- a/getters.go +++ /dev/null @@ -1,61 +0,0 @@ -package go_sd_jwt - -// Body returns the body of the JWT -func (s *SdJwt) Body() *map[string]any { - return &s.body -} - -// Token returns the JWT token as it was received -func (s *SdJwt) Token() string { - return s.token -} - -// Signature returns the signature of the provided token used to verify it -func (s *SdJwt) Signature() string { - return s.signature -} - -// Head returns the head of the JWT -func (s *SdJwt) Head() map[string]any { - return s.head -} - -// Disclosures returns the disclosures of the SD-JWT -func (s *SdJwt) Disclosures() []Disclosure { - return s.disclosures -} - -// PublicKey returns the public key json (if provided) -func (s *SdJwt) PublicKey() string { - return s.publicKey -} - -// KbJwt returns the signed kb-jwt (if provided) -func (s *SdJwt) KbJwt() *string { - return s.kbJwt -} - -// ClaimName returns the claim name of the disclosure -func (d *Disclosure) ClaimName() *string { - return d.claimName -} - -// ClaimValue returns the claim value of the disclosure -func (d *Disclosure) ClaimValue() string { - return d.claimValue -} - -// Salt returns the salt of the disclosure -func (d *Disclosure) Salt() string { - return d.salt -} - -// RawValue returns the decoded contents of the disclosure -func (d *Disclosure) RawValue() string { - return d.rawValue -} - -// EncodedValue returns the disclosure as it was listed in the original SD-JWT -func (d *Disclosure) EncodedValue() string { - return d.encodedValue -} diff --git a/go.mod b/go.mod index bc53aad..0e1d3cb 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,10 @@ module github.com/MichaelFraser99/go-sd-jwt go 1.21.0 -require github.com/stretchr/testify v1.8.4 +require ( + github.com/MichaelFraser99/go-jose v0.3.0 + github.com/stretchr/testify v1.8.4 +) require ( github.com/davecgh/go-spew v1.1.1 // indirect diff --git a/go.sum b/go.sum index fa4b6e6..d71f210 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/MichaelFraser99/go-jose v0.3.0 h1:7tWn9TaU2ANMeq7OTicR3OBtePNyqZI86iu/wd83b4Y= +github.com/MichaelFraser99/go-jose v0.3.0/go.mod h1:kdRvg7/FPcDnsEz8PyCg5hhcBlLud9F0jB4Xy/u771c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= diff --git a/internal/error/error.go b/internal/error/error.go index eed2ac7..5e73c5c 100644 --- a/internal/error/error.go +++ b/internal/error/error.go @@ -2,14 +2,8 @@ package error import "errors" -type InvalidToken struct { - Message string -} - -func (e *InvalidToken) Error() string { - return e.Message -} - -var InvalidJsonError = errors.New("") -var UnknownDisclosureError = errors.New("") -var ClaimNotFoundError = errors.New("") +var InvalidToken = errors.New("") +var InvalidJson = errors.New("") +var InvalidDisclosure = errors.New("") +var UnknownDisclosure = errors.New("") +var ClaimNotFound = errors.New("") diff --git a/internal/jose/algorithms/common/elliptic-curve.go b/internal/jose/algorithms/common/elliptic-curve.go deleted file mode 100644 index 1c3a6a0..0000000 --- a/internal/jose/algorithms/common/elliptic-curve.go +++ /dev/null @@ -1,125 +0,0 @@ -package common - -import ( - "crypto/ecdsa" - "crypto/elliptic" - "crypto/rand" - "encoding/base64" - "encoding/json" - "fmt" - e "github.com/MichaelFraser99/go-sd-jwt/internal/jose/error" - "math/big" -) - -type PublicKey struct { - Kty string `json:"kty"` - Crv string `json:"crv"` - X string `json:"x"` - Y string `json:"y"` -} - -func (pubKey *PublicKey) Equal(x PublicKey) bool { - if pubKey.X == x.X && pubKey.Y == x.Y && pubKey.Kty == x.Kty && pubKey.Crv == x.Crv { - return true - } - return false -} - -func NewPublicKeyFromJson(publicKeyJson string, curve elliptic.Curve) (*ecdsa.PublicKey, error) { - var publicKey PublicKey - err := json.Unmarshal([]byte(publicKeyJson), &publicKey) - if err != nil { - return nil, &e.InvalidPublicKey{Message: fmt.Sprintf("provided public key json isn't valid es256 public key: %s", err.Error())} - } - - xBytes, err := base64.RawURLEncoding.DecodeString(publicKey.X) - if err != nil { - return nil, &e.InvalidPublicKey{Message: fmt.Sprintf("error decoding provided public key: %s", err.Error())} - } - - yBytes, err := base64.RawURLEncoding.DecodeString(publicKey.Y) - if err != nil { - return nil, &e.InvalidPublicKey{Message: fmt.Sprintf("error decoding provided public key: %s", err.Error())} - } - - pk := &ecdsa.PublicKey{ - Curve: curve, - X: big.NewInt(0).SetBytes(xBytes), - Y: big.NewInt(0).SetBytes(yBytes), - } - return pk, nil -} - -func ExtractRSFromSignature(signature string, keySize int) (*big.Int, *big.Int, error) { - decodedSignature, err := base64.RawURLEncoding.DecodeString(signature) - if err != nil { - return nil, nil, &e.InvalidSignature{Message: fmt.Sprintf("error decoding signature: %s", err.Error())} - } - - if len(decodedSignature) != keySize { - return nil, nil, &e.InvalidSignature{Message: fmt.Sprintf("signature should be %d bytes for given algorithm", keySize)} - } - rb := decodedSignature[:keySize/2] - sb := decodedSignature[keySize/2:] - - r := big.NewInt(0).SetBytes(rb) - s := big.NewInt(0).SetBytes(sb) - - return r, s, nil -} - -func GenerateToken(alg string, header map[string]string, body map[string]any) (*string, error) { - h := header - h["alg"] = alg - - headerBytes, err := json.Marshal(h) - if err != nil { - return nil, &e.SigningError{Message: fmt.Sprintf("failed to marshal header to bytes: %s", err.Error())} - } - base64Header := base64.RawURLEncoding.EncodeToString(headerBytes) - - bodyBytes, err := json.Marshal(body) - if err != nil { - return nil, &e.SigningError{Message: fmt.Sprintf("failed to marshal body to bytes: %s", err.Error())} - } - base64Body := base64.RawURLEncoding.EncodeToString(bodyBytes) - - token := fmt.Sprintf("%s.%s", base64Header, base64Body) - return &token, nil -} - -func SignToken(token string, pk ecdsa.PrivateKey, digest []byte, keySize int) (*string, error) { - r, s, err := ecdsa.Sign(rand.Reader, &pk, digest) - if err != nil { - return nil, &e.SigningError{Message: fmt.Sprintf("failed to sign token: %s", err.Error())} - } - - sigBytes := make([]byte, keySize) - - r.FillBytes(sigBytes[0 : keySize/2]) - s.FillBytes(sigBytes[keySize/2:]) - - base64Sig := base64.RawURLEncoding.EncodeToString(sigBytes) - signedToken := fmt.Sprintf("%s.%s", token, base64Sig) - return &signedToken, nil -} - -func GeneratePublicKey(pk ecdsa.PrivateKey, curveName string, keySize int) PublicKey { - cryptoPubKey := pk.PublicKey - - xb := make([]byte, keySize/2) - yb := make([]byte, keySize/2) - - cryptoPubKey.X.FillBytes(xb) - cryptoPubKey.Y.FillBytes(yb) - - x := base64.RawURLEncoding.EncodeToString(xb) - y := base64.RawURLEncoding.EncodeToString(yb) - - return PublicKey{ - Kty: "EC", - Crv: curveName, - X: x, - Y: y, - } -} diff --git a/internal/jose/algorithms/es256/ES256.go b/internal/jose/algorithms/es256/ES256.go deleted file mode 100644 index 7da1d23..0000000 --- a/internal/jose/algorithms/es256/ES256.go +++ /dev/null @@ -1,57 +0,0 @@ -package es256 - -import ( - "crypto" - "crypto/ecdsa" - "crypto/elliptic" - "crypto/rand" - "crypto/sha256" - "fmt" - "github.com/MichaelFraser99/go-sd-jwt/internal/jose/algorithms/common" - e "github.com/MichaelFraser99/go-sd-jwt/internal/jose/error" -) - -type ES256 struct{} - -func (signer *ES256) ValidateSignature(token, signature string, publicKeyJson string) (bool, error) { - pk, err := common.NewPublicKeyFromJson(publicKeyJson, elliptic.P256()) - if err != nil { - return false, err - } - - bodyHash := sha256.Sum256([]byte(token)) - - r, s, err := common.ExtractRSFromSignature(signature, 64) - if err != nil { - return false, err - } - - return ecdsa.Verify(pk, bodyHash[:], r, s), nil -} - -func (signer *ES256) Sign(body map[string]any, headerKeys map[string]string) (*string, crypto.PrivateKey, crypto.PublicKey, error) { - curve := elliptic.P256() - pk, err := ecdsa.GenerateKey(curve, rand.Reader) - if err != nil { - return nil, nil, nil, &e.SigningError{Message: fmt.Sprintf("failed to generate key: %s", err.Error())} - } - - token, err := common.GenerateToken("ES256", headerKeys, body) - if err != nil { - return nil, nil, nil, err - } - - digest := sha256.Sum256([]byte(*token)) - - signedToken, err := common.SignToken(*token, *pk, digest[:], 64) - if err != nil { - return nil, nil, nil, err - } - - pubKey := common.GeneratePublicKey(*pk, "P-256", 64) - - return signedToken, pk, pubKey, nil -} -func (signer *ES256) SignWithKey(body map[string]any, headerKeys map[string]string, privateKey string) (*string, error) { - return nil, nil //todo -} diff --git a/internal/jose/algorithms/es256/ES256_test.go b/internal/jose/algorithms/es256/ES256_test.go deleted file mode 100644 index c57a41b..0000000 --- a/internal/jose/algorithms/es256/ES256_test.go +++ /dev/null @@ -1,79 +0,0 @@ -package es256 - -import ( - "encoding/json" - "strings" - "testing" -) - -func TestValidateSignatureES256(t *testing.T) { - publicKey := "{\"kty\":\"EC\",\"crv\":\"P-256\",\"x\":\"b28d4MwZMjw8-00CG4xfnn9SLMVMM19SlqZpVb_uNtQ\",\"y\":\"Xv5zWwuoaTgdS6hV43yI6gBwTnjukmFQQnJ_kCxzqk8\"}" - - token := "eyJhbGciOiAiRVMyNTYifQ.eyJfc2QiOiBbIkNyUWU3UzVrcUJBSHQtbk1ZWGdjNmJkdDJTSDVhVFkxc1VfTS1QZ2tqUEkiLCAiSnpZakg0c3ZsaUgwUjNQeUVNZmVadTZKdDY5dTVxZWhabzdGN0VQWWxTRSIsICJQb3JGYnBLdVZ1Nnh5bUphZ3ZrRnNGWEFiUm9jMkpHbEFVQTJCQTRvN2NJIiwgIlRHZjRvTGJnd2Q1SlFhSHlLVlFaVTlVZEdFMHc1cnREc3JaemZVYW9tTG8iLCAiWFFfM2tQS3QxWHlYN0tBTmtxVlI2eVoyVmE1TnJQSXZQWWJ5TXZSS0JNTSIsICJYekZyendzY002R242Q0pEYzZ2Vks4QmtNbmZHOHZPU0tmcFBJWmRBZmRFIiwgImdiT3NJNEVkcTJ4Mkt3LXc1d1BFemFrb2I5aFYxY1JEMEFUTjNvUUw5Sk0iLCAianN1OXlWdWx3UVFsaEZsTV8zSmx6TWFTRnpnbGhRRzBEcGZheVF3TFVLNCJdLCAiaXNzIjogImh0dHBzOi8vZXhhbXBsZS5jb20vaXNzdWVyIiwgImlhdCI6IDE2ODMwMDAwMDAsICJleHAiOiAxODgzMDAwMDAwLCAic3ViIjogInVzZXJfNDIiLCAibmF0aW9uYWxpdGllcyI6IFt7Ii4uLiI6ICJwRm5kamtaX1ZDem15VGE2VWpsWm8zZGgta284YUlLUWM5RGxHemhhVllvIn0sIHsiLi4uIjogIjdDZjZKa1B1ZHJ5M2xjYndIZ2VaOGtoQXYxVTFPU2xlclAwVmtCSnJXWjAifV0sICJfc2RfYWxnIjogInNoYS0yNTYiLCAiY25mIjogeyJqd2siOiB7Imt0eSI6ICJFQyIsICJjcnYiOiAiUC0yNTYiLCAieCI6ICJUQ0FFUjE5WnZ1M09IRjRqNFc0dmZTVm9ISVAxSUxpbERsczd2Q2VHZW1jIiwgInkiOiAiWnhqaVdXYlpNUUdIVldLVlE0aGJTSWlyc1ZmdWVjQ0U2dDRqVDlGMkhaUSJ9fX0" - - signature := "kmx687kUBiIDvKWgo2Dub-TpdCCRLZwtD7TOj4RoLsUbtFBI8sMrtH2BejXtm_P6fOAjKAVc_7LRNJFgm3PJhg" - - es256 := &ES256{} - - valid, err := es256.ValidateSignature(token, signature, publicKey) - if err != nil { - t.Error("no error should be thrown", err) - } - if !valid { - t.Error("signature is not valid") - } -} - -func TestES256_Sign(t *testing.T) { - body := map[string]any{ - "firstname": "john", - "surname": "smith", - "address": map[string]string{ - "street": "Long Lane", - "number": "15", - "city": "Edinburgh", - }, - } - - headerKeys := map[string]string{ - "typ": "jwt", - } - - es256 := &ES256{} - - token, privateKey, publicKey, err := es256.Sign(body, headerKeys) - if err != nil { - t.Error("no error should be thrown", err) - t.FailNow() - } - if token == nil { - t.Error("token should not be nil") - t.FailNow() - } - if privateKey == nil { - t.Error("private key should not be nil") - t.FailNow() - } - if publicKey == nil { - t.Error("public key should not be nil") - t.FailNow() - } - jsonPk, err := json.Marshal(publicKey) - if err != nil { - t.Error("no error should be thrown", err) - t.FailNow() - } - t.Log(*token) - t.Log(string(jsonPk)) - - components := strings.Split(*token, ".") - valid, err := es256.ValidateSignature(strings.Join(components[0:2], "."), components[2], string(jsonPk)) - if err != nil { - t.Error("no error should be thrown", err) - t.FailNow() - } - if !valid { - t.Error("signature is not valid") - t.FailNow() - } -} diff --git a/internal/jose/algorithms/es384/ES384.go b/internal/jose/algorithms/es384/ES384.go deleted file mode 100644 index 1d63a87..0000000 --- a/internal/jose/algorithms/es384/ES384.go +++ /dev/null @@ -1,57 +0,0 @@ -package es384 - -import ( - "crypto" - "crypto/ecdsa" - "crypto/elliptic" - "crypto/rand" - "crypto/sha512" - "fmt" - "github.com/MichaelFraser99/go-sd-jwt/internal/jose/algorithms/common" - e "github.com/MichaelFraser99/go-sd-jwt/internal/jose/error" -) - -type ES384 struct{} - -func (signer *ES384) ValidateSignature(token, signature string, publicKeyJson string) (bool, error) { - pk, err := common.NewPublicKeyFromJson(publicKeyJson, elliptic.P384()) - if err != nil { - return false, err - } - - bodyHash := sha512.Sum384([]byte(token)) - - r, s, err := common.ExtractRSFromSignature(signature, 96) - if err != nil { - return false, err - } - - return ecdsa.Verify(pk, bodyHash[:], r, s), nil -} - -func (signer *ES384) Sign(body map[string]any, headerKeys map[string]string) (*string, crypto.PrivateKey, crypto.PublicKey, error) { - curve := elliptic.P384() - pk, err := ecdsa.GenerateKey(curve, rand.Reader) - if err != nil { - return nil, nil, nil, &e.SigningError{Message: fmt.Sprintf("failed to generate key: %s", err.Error())} - } - - token, err := common.GenerateToken("ES384", headerKeys, body) - if err != nil { - return nil, nil, nil, err - } - - digest := sha512.Sum384([]byte(*token)) - - signedToken, err := common.SignToken(*token, *pk, digest[:], 96) - if err != nil { - return nil, nil, nil, err - } - - pubKey := common.GeneratePublicKey(*pk, "P-384", 96) - - return signedToken, pk, pubKey, nil -} -func (signer *ES384) SignWithKey(body map[string]any, headerKeys map[string]string, privateKey string) (*string, error) { - return nil, nil //todo -} diff --git a/internal/jose/algorithms/es384/ES384_test.go b/internal/jose/algorithms/es384/ES384_test.go deleted file mode 100644 index 5385624..0000000 --- a/internal/jose/algorithms/es384/ES384_test.go +++ /dev/null @@ -1,77 +0,0 @@ -package es384 - -import ( - "encoding/json" - "strings" - "testing" -) - -func TestValidateSignatureES384(t *testing.T) { - publicKey := "{\"kty\":\"EC\",\"crv\":\"P-384\",\"x\":\"AgQPgcqypazyTOW8CsQOhnN2jXSLrUha6YrkXAZES6sOWT44t_OSx68kEg-UQ1lo\",\"y\":\"uRYLEPxefzGME223BsBLDyhDJ7KZApkKdmXbvaZorFQol8beG6zfve3Z16Jq1Xrj\"}" - - token := "eyJhbGciOiJFUzM4NCIsInR5cCI6Imp3dCJ9.eyJhZGRyZXNzIjp7ImNpdHkiOiJFZGluYnVyZ2giLCJudW1iZXIiOiIxNSIsInN0cmVldCI6IkxvbmcgTGFuZSJ9LCJmaXJzdG5hbWUiOiJqb2huIiwic3VybmFtZSI6InNtaXRoIn0" - - signature := "T1wWViEJKvYoOIYTD3WtK69cJMJTAmaAXni54AcWBLmOmiYQCIigzynawj5Fe1L4MRqmiCHdRF7F3Uz_ab_QvDhQw925k7rHWTwL2eSmK8TRRIS598MEM0VbcBL7AAbN" - - es384 := &ES384{} - - valid, err := es384.ValidateSignature(token, signature, publicKey) - if err != nil { - t.Error("no error should be thrown", err) - } - if !valid { - t.Error("signature is not valid") - } -} - -func TestES384_Sign(t *testing.T) { - body := map[string]any{ - "firstname": "john", - "surname": "smith", - "address": map[string]string{ - "street": "Long Lane", - "number": "15", - "city": "Edinburgh", - }, - } - - headerKeys := map[string]string{ - "typ": "jwt", - } - - es384 := &ES384{} - - token, privateKey, publicKey, err := es384.Sign(body, headerKeys) - if err != nil { - t.Error("no error should be thrown", err) - t.FailNow() - } - if token == nil { - t.Error("token should not be nil") - t.FailNow() - } - if privateKey == nil { - t.Error("private key should not be nil") - t.FailNow() - } - if publicKey == nil { - t.Error("public key should not be nil") - t.FailNow() - } - jsonPk, err := json.Marshal(publicKey) - if err != nil { - t.Error("no error should be thrown:", err) - t.FailNow() - } - t.Log(*token) - t.Log(string(jsonPk)) - - components := strings.Split(*token, ".") - valid, err := es384.ValidateSignature(strings.Join(components[0:2], "."), components[2], string(jsonPk)) - if err != nil { - t.Error("no error should be thrown:", err) - } - if !valid { - t.Error("signature is not valid") - } -} diff --git a/internal/jose/algorithms/es512/ES512.go b/internal/jose/algorithms/es512/ES512.go deleted file mode 100644 index a476d74..0000000 --- a/internal/jose/algorithms/es512/ES512.go +++ /dev/null @@ -1,57 +0,0 @@ -package es512 - -import ( - "crypto" - "crypto/ecdsa" - "crypto/elliptic" - "crypto/rand" - "crypto/sha512" - "fmt" - "github.com/MichaelFraser99/go-sd-jwt/internal/jose/algorithms/common" - e "github.com/MichaelFraser99/go-sd-jwt/internal/jose/error" -) - -type ES512 struct{} - -func (signer *ES512) ValidateSignature(token, signature string, publicKeyJson string) (bool, error) { - pk, err := common.NewPublicKeyFromJson(publicKeyJson, elliptic.P521()) - if err != nil { - return false, err - } - - bodyHash := sha512.Sum512([]byte(token)) - - r, s, err := common.ExtractRSFromSignature(signature, 132) - if err != nil { - return false, err - } - - return ecdsa.Verify(pk, bodyHash[:], r, s), nil -} - -func (signer *ES512) Sign(body map[string]any, headerKeys map[string]string) (*string, crypto.PrivateKey, crypto.PublicKey, error) { - curve := elliptic.P521() - pk, err := ecdsa.GenerateKey(curve, rand.Reader) - if err != nil { - return nil, nil, nil, &e.SigningError{Message: fmt.Sprintf("failed to generate key: %s", err.Error())} - } - - token, err := common.GenerateToken("ES512", headerKeys, body) - if err != nil { - return nil, nil, nil, err - } - - digest := sha512.Sum512([]byte(*token)) - - signedToken, err := common.SignToken(*token, *pk, digest[:], 132) - if err != nil { - return nil, nil, nil, err - } - - pubKey := common.GeneratePublicKey(*pk, "P-521", 132) - - return signedToken, pk, pubKey, nil -} -func (signer *ES512) SignWithKey(body map[string]any, headerKeys map[string]string, privateKey string) (*string, error) { - return nil, nil //todo -} diff --git a/internal/jose/algorithms/es512/ES512_test.go b/internal/jose/algorithms/es512/ES512_test.go deleted file mode 100644 index ce2c814..0000000 --- a/internal/jose/algorithms/es512/ES512_test.go +++ /dev/null @@ -1,77 +0,0 @@ -package es512 - -import ( - "encoding/json" - "strings" - "testing" -) - -func TestValidateSignatureES512(t *testing.T) { - publicKey := "{\"kty\":\"EC\",\"crv\":\"P-521\",\"x\":\"Ae9hYaIls2sRK8n1XddHMjeS592yBIanCf8skWNbPPgez00w1m_xVt9BANFrQnZQgzoE0kBhOSVidRazi1QcY-3k\",\"y\":\"AX7Q--gyprCTZUDDPv48nNLtlbhCvC1aXxtc4pYpLFQbBIkeDXz0aMbBTyqs6sJZU0tDjeKohDTjwg3-3dbZLCm4\"}" - - token := "eyJhbGciOiJFUzUxMiIsInR5cCI6Imp3dCJ9.eyJhZGRyZXNzIjp7ImNpdHkiOiJFZGluYnVyZ2giLCJudW1iZXIiOiIxNSIsInN0cmVldCI6IkxvbmcgTGFuZSJ9LCJmaXJzdG5hbWUiOiJqb2huIiwic3VybmFtZSI6InNtaXRoIn0" - - signature := "ADT4CJwauvGdsZ1739n9iT0_HYq0om0h-UirM5CZQEwAmfj6cGgHR-M2cDZCq5dDXvKISY5ZqBrOLk_uNeQv0ZzNAJ_6Jmz_Sa3sClp-uHLAGAiKOYx7l_aFSN4_rxq2vQFXbfsclREdQTv_8W-u5ax8SWLyNHxaNn7nYKpssmGaokTs" - - es512 := &ES512{} - - valid, err := es512.ValidateSignature(token, signature, publicKey) - if err != nil { - t.Error("no error should be thrown", err) - } - if !valid { - t.Error("signature is not valid") - } -} - -func TestES512_Sign(t *testing.T) { - body := map[string]any{ - "firstname": "john", - "surname": "smith", - "address": map[string]string{ - "street": "Long Lane", - "number": "15", - "city": "Edinburgh", - }, - } - - headerKeys := map[string]string{ - "typ": "jwt", - } - - es512 := &ES512{} - - token, privateKey, publicKey, err := es512.Sign(body, headerKeys) - if err != nil { - t.Error("no error should be thrown", err) - t.FailNow() - } - if token == nil { - t.Error("token should not be nil") - t.FailNow() - } - if privateKey == nil { - t.Error("private key should not be nil") - t.FailNow() - } - if publicKey == nil { - t.Error("public key should not be nil") - t.FailNow() - } - jsonPk, err := json.Marshal(publicKey) - if err != nil { - t.Error("no error should be thrown", err) - t.FailNow() - } - t.Log(*token) - t.Log(string(jsonPk)) - - components := strings.Split(*token, ".") - valid, err := es512.ValidateSignature(strings.Join(components[0:2], "."), components[2], string(jsonPk)) - if err != nil { - t.Error("no error should be thrown", err) - } - if !valid { - t.Error("signature is not valid") - } -} diff --git a/internal/jose/error/error.go b/internal/jose/error/error.go deleted file mode 100644 index 755c8d1..0000000 --- a/internal/jose/error/error.go +++ /dev/null @@ -1,33 +0,0 @@ -package error - -type InvalidSignature struct { - Message string -} - -func (e *InvalidSignature) Error() string { - return e.Message -} - -type UnsupportedAlgorithm struct { - Message string -} - -func (e *UnsupportedAlgorithm) Error() string { - return e.Message -} - -type InvalidPublicKey struct { - Message string -} - -func (e *InvalidPublicKey) Error() string { - return e.Message -} - -type SigningError struct { - Message string -} - -func (e *SigningError) Error() string { - return e.Message -} diff --git a/internal/jose/signature.go b/internal/jose/signature.go deleted file mode 100644 index 597afb2..0000000 --- a/internal/jose/signature.go +++ /dev/null @@ -1,33 +0,0 @@ -package jose - -import ( - "crypto" - "fmt" - "github.com/MichaelFraser99/go-sd-jwt/internal/jose/algorithms/es256" - "github.com/MichaelFraser99/go-sd-jwt/internal/jose/algorithms/es384" - "github.com/MichaelFraser99/go-sd-jwt/internal/jose/algorithms/es512" - e "github.com/MichaelFraser99/go-sd-jwt/internal/jose/error" -) - -type Signer interface { - ValidateSignature(token, signature string, publicKey string) (bool, error) - Sign(body map[string]any, headerKeys map[string]string) (*string, crypto.PrivateKey, crypto.PublicKey, error) - SignWithKey(body map[string]any, headerKeys map[string]string, privateKey string) (*string, error) -} - -func GetSigner(alg string) (Signer, error) { - var s Signer - switch alg { - case "ES256": - s = &es256.ES256{} - case "ES384": - s = &es384.ES384{} - case "ES512": - s = &es512.ES512{} - - default: - return nil, &e.UnsupportedAlgorithm{Message: fmt.Sprintf("unsupported algorithm: '%s'", alg)} - } - - return s, nil -} diff --git a/internal/model/model.go b/internal/model/model.go new file mode 100644 index 0000000..624e38e --- /dev/null +++ b/internal/model/model.go @@ -0,0 +1,13 @@ +package model + +type JwsSdJwt struct { + Payload *string `json:"payload"` + Protected *string `json:"protected"` + Signature *string `json:"signature"` + Disclosures []string `json:"disclosures"` + KbJwt *string `json:"kb_jwt"` +} + +type ArrayDisclosure struct { + Digest *string `json:"..."` +} diff --git a/internal/salt/salt.go b/internal/salt/salt.go new file mode 100644 index 0000000..0b75471 --- /dev/null +++ b/internal/salt/salt.go @@ -0,0 +1,17 @@ +package salt + +import ( + "crypto/rand" + "encoding/base64" + "fmt" +) + +func NewSalt() (*string, error) { + randomBytes := make([]byte, 128) + _, err := rand.Read(randomBytes) + if err != nil { + return nil, fmt.Errorf("error generating salt value: %w", err) + } + saltValue := base64.RawURLEncoding.EncodeToString(randomBytes) + return &saltValue, nil +} diff --git a/internal/utils/utils.go b/internal/utils/utils.go new file mode 100644 index 0000000..935e270 --- /dev/null +++ b/internal/utils/utils.go @@ -0,0 +1,307 @@ +package utils + +import ( + "encoding/base64" + "encoding/json" + "errors" + "github.com/MichaelFraser99/go-sd-jwt/disclosure" + "github.com/MichaelFraser99/go-sd-jwt/internal/model" + "reflect" + "strings" +) + +func ValidateArrayClaims(s *[]any, currentDisclosure *disclosure.Disclosure, base64HashedDisclosure string) (found bool, err error) { + for i, v := range *s { + + switch reflect.TypeOf(v).Kind() { + + case reflect.Slice: + found, err = ValidateArrayClaims(PointerSlice(v.([]any)), currentDisclosure, base64HashedDisclosure) + if err != nil { + return false, err + } + if found { + return true, nil + } + + case reflect.Map: + ad := &model.ArrayDisclosure{} + vb, err := json.Marshal(v) + if err != nil { + return false, err + } + + _ = json.Unmarshal(vb, ad) + + if ad.Digest != nil { + if *ad.Digest == base64HashedDisclosure { + (*s)[i] = currentDisclosure.Value + return true, nil + } + } + + found, err = ValidateSDClaims(PointerMap(v.(map[string]any)), currentDisclosure, base64HashedDisclosure) + if err != nil { + return false, err + } + if found { + return true, nil + } + } + } + + return false, nil +} + +func ValidateSDClaims(values *map[string]any, currentDisclosure *disclosure.Disclosure, base64HashedDisclosure string) (found bool, err error) { + if _, ok := (*values)["_sd"]; ok { + for _, digest := range (*values)["_sd"].([]any) { + sDigest := digest.(string) + if sDigest == base64HashedDisclosure { + if currentDisclosure.Key != nil { + (*values)[*currentDisclosure.Key] = currentDisclosure.Value + return true, nil + } else { + return false, errors.New("invalid disclosure format for _sd claim") + } + } + } + } + + for k, v := range *values { + if k != "_sd" && k != "_sd_alg" { + if reflect.TypeOf(v).Kind() == reflect.Slice { + found, err = ValidateArrayClaims(PointerSlice(v.([]any)), currentDisclosure, base64HashedDisclosure) + if err != nil { + return false, err + } + } else if reflect.TypeOf(v).Kind() == reflect.Map { + found, err = ValidateSDClaims(PointerMap(v.(map[string]any)), currentDisclosure, base64HashedDisclosure) + if err != nil { + return found, err + } + } + if found { + return true, nil + } + } + } + return false, nil +} + +func GetDigests(m map[string]any) []any { + var digests []any + for k, v := range m { + if reflect.TypeOf(v).Kind() == reflect.Map { + digests = append(digests, GetDigests(v.(map[string]any))...) + } else if k == "_sd" { + digests = append(digests, v.([]any)...) + } else if reflect.TypeOf(v).Kind() == reflect.Slice { + for _, v2 := range v.([]any) { + b, err := json.Marshal(v2) + if err == nil { + var ArrayDisclosure model.ArrayDisclosure + err = json.Unmarshal(b, &ArrayDisclosure) + if err == nil && ArrayDisclosure.Digest != nil { + digests = append(digests, *ArrayDisclosure.Digest) + } + } + } + } + } + return digests +} + +func StripSDClaimsFromSlice(input []any) []any { + output := make([]any, len(input)) + for i, v := range input { + switch reflect.TypeOf(v).Kind() { + case reflect.Map: + output[i] = StripSDClaims(v.(map[string]any)) + case reflect.Slice: + output[i] = StripSDClaimsFromSlice(v.([]any)) + default: + output[i] = v + } + } + return output +} + +func StripSDClaims(body map[string]any) map[string]any { + bodyMap := make(map[string]any) + for k, v := range body { + switch reflect.TypeOf(v).Kind() { + case reflect.Map: + bodyMap[k] = StripSDClaims(v.(map[string]any)) + case reflect.Slice: + if k != "_sd" { + bodyMap[k] = StripSDClaimsFromSlice(v.([]any)) + } + default: + if k != "_sd_alg" { + bodyMap[k] = v + } + } + } + return bodyMap +} + +func StringifyDisclosures(disclosures []disclosure.Disclosure) string { + result := "[" + for i, d := range disclosures { + if d.Key != nil { + result += "(" + *d.Key + ") " + } else { + result += " " + } + result += d.Value.(string) + " " + if i != len(disclosures)-1 { + result += "," + } + } + result += "]" + return result +} + +func ValidateDigests(body map[string]any) error { + digests := GetDigests(body) + + for _, d := range digests { + count := 0 + for _, d2 := range digests { + if d == d2 { + count++ + } + } + if count > 1 { + return errors.New("duplicate digest found") + } + } + return nil +} + +func ValidateDisclosures(disclosures []string) ([]disclosure.Disclosure, error) { + var disclosureArray []disclosure.Disclosure + + if len(disclosures) == 0 { + return nil, errors.New("token has no specified disclosures") + } + + for _, d := range disclosures { + count := 0 + if d != "" { + for _, d2 := range disclosures { + if d == d2 { + count++ + } + } + if count > 1 { + return nil, errors.New("duplicate disclosure found") + } + dis, err := disclosure.NewFromDisclosure(d) + if err != nil { + return nil, err + } + disclosureArray = append(disclosureArray, *dis) + } + } + return disclosureArray, nil +} + +func CheckForKbJwt(candidate string) *string { + if !strings.Contains(candidate, ".") { + return nil + } + + sections := strings.Split(candidate, ".") + if len(sections) != 3 { + return nil + } + + return &candidate +} + +func ValidateKbJwt(kbJwt string, sdJwtBody map[string]any) error { + kbjc := strings.Split(kbJwt, ".") + + if len(kbjc) != 3 { + return errors.New("kb jwt is in an invalid format") + } + + //head + kbhb, err := base64.RawURLEncoding.DecodeString(kbjc[0]) + if err != nil { + return err + } + var kbh map[string]any + err = json.Unmarshal(kbhb, &kbh) + if err != nil { + return err + } + + //body + kbbb, err := base64.RawURLEncoding.DecodeString(kbjc[1]) + if err != nil { + return err + } + var kbb map[string]any + err = json.Unmarshal(kbbb, &kbb) + if err != nil { + return err + } + + //validate kb jwt contents + if kbh["typ"] != "kb+jwt" { + return errors.New("kb jwt is not of type kb+jwt") + } + + return nil +} + +// Pointer is a helper method that returns a pointer to the given value. +func Pointer[T comparable](t T) *T { + return &t +} + +// PointerMap is a helper method that returns a pointer to the given map. +func PointerMap(m map[string]any) *map[string]any { + return &m +} + +// PointerSlice is a helper method that returns a pointer to the given slice. +func PointerSlice(s []any) *[]any { + return &s +} + +func CopyMap(m map[string]any) map[string]any { + cp := make(map[string]any) + for k, v := range m { + vm, mapOk := v.(map[string]any) + vs, sliceOk := v.([]any) + if mapOk { + cp[k] = CopyMap(vm) + } else if sliceOk { + cp[k] = CopySlice(vs) + } else { + cp[k] = v + } + } + + return cp +} + +func CopySlice(s []any) []any { + cp := make([]any, len(s)) + for i, v := range s { + vm, mapOk := v.(map[string]any) + vs, sliceOk := v.([]any) + if mapOk { + cp[i] = CopyMap(vm) + } else if sliceOk { + cp[i] = CopySlice(vs) + } else { + cp[i] = v + } + } + return cp +} diff --git a/sd-jwt.go b/sd-jwt.go index cad3715..e675b34 100644 --- a/sd-jwt.go +++ b/sd-jwt.go @@ -5,176 +5,224 @@ package go_sd_jwt import ( "crypto" + "crypto/rand" "crypto/sha256" "crypto/sha512" "encoding/base64" "encoding/json" "errors" "fmt" + "github.com/MichaelFraser99/go-sd-jwt/disclosure" e "github.com/MichaelFraser99/go-sd-jwt/internal/error" - "github.com/MichaelFraser99/go-sd-jwt/internal/jose" + "github.com/MichaelFraser99/go-sd-jwt/internal/model" + "github.com/MichaelFraser99/go-sd-jwt/internal/utils" "hash" - "reflect" "slices" "strings" + "time" ) // SdJwt this object represents a valid SD-JWT. Created using the FromToken function which performs the required validation. // Helper methods are provided for retrieving the contents type SdJwt struct { - token string head map[string]any body map[string]any signature string - publicKey string kbJwt *string - disclosures []Disclosure + disclosures []disclosure.Disclosure } -// Disclosure this object represents a single disclosure in a SD-JWT. -// Helper methods are provided for retrieving the contents -type Disclosure struct { - salt string - claimName *string - claimValue string - rawValue string - encodedValue string -} - -type jwsSdJwt struct { - Payload *string `json:"payload"` - Protected *string `json:"protected"` - Signature *string `json:"signature"` - Disclosures []string `json:"disclosures"` - KbJwt *string `json:"kb_jwt"` -} - -type arrayDisclosure struct { - Digest *string `json:"..."` +// New +// Creates a new SD-JWT from a JWT format token. +// The token is validated inline with the SD-JWT specification. +// If the token is valid, a new SdJwt object is returned. +// If a kb-jwt is included, the contents of this too will be validated. +func New(token string) (*SdJwt, error) { + return validateJwt(token) } -// FromToken -// Creates a new SD-JWT from a JWS or JWT format token. +// NewFromJws +// Creates a new SD-JWT from a JWS format token. // The token is validated inline with the SD-JWT specification. // If the token is valid, a new SdJwt object is returned. -// The signature will be validated using the provided public key. -// The public key must be provided in JSON jwk format. -// If a cnf claim is present in the token AND the sd-jwt was sent with a kb-jwt, the kb-jwt will be validated. -func FromToken(token string, publicKey string) (*SdJwt, error) { - jwsSdjwt := jwsSdJwt{} +// If a kb-jwt is included, the contents of this too will be validated. +func NewFromJws(token string) (*SdJwt, error) { + jwsSdjwt := model.JwsSdJwt{} err := json.Unmarshal([]byte(token), &jwsSdjwt) - if err == nil { - if jwsSdjwt.Payload != nil && jwsSdjwt.Protected != nil && jwsSdjwt.Signature != nil { - return validateJws(jwsSdjwt, publicKey) - } else { - return nil, &e.InvalidToken{Message: "invalid JWS format SD-JWT provided"} - } + if err != nil { + return nil, fmt.Errorf("%winvalid JSON provided", e.InvalidToken) + } + + if jwsSdjwt.Payload != nil && jwsSdjwt.Protected != nil && jwsSdjwt.Signature != nil { + return validateJws(jwsSdjwt) } else { - return validateJwt(token, publicKey) + return nil, fmt.Errorf("%winvalid JWS format SD-JWT provided", e.InvalidToken) } } -func validateJws(token jwsSdJwt, publicKey string) (*SdJwt, error) { - sdJwt := &SdJwt{} +// todo: refactor this - its not overly flexible +func (s *SdJwt) AddKeyBindingJwt(signer crypto.Signer, hash crypto.Hash, alg, aud, nonce string) error { + if s.kbJwt != nil { + return errors.New("key binding jwt already exists") + } - sdJwt.publicKey = publicKey - sdJwt.kbJwt = token.KbJwt + sdAlg, ok := s.body["_sd_alg"].(string) + if (ok && !strings.EqualFold(sdAlg, hash.String())) || (!ok && strings.ToLower(hash.String()) != "sha-256") { + return errors.New("key binding jwt hashing algorithm does not match the hashing algorithm specified in the sd-jwt - if sd-jwt does not specify a hashing algorithm, sha-256 is selected by default") + } - b, err := json.Marshal(token) - if err != nil { - return nil, &e.InvalidToken{Message: fmt.Sprintf("failed to json parse provided jws token: %s", err.Error())} + kbHead := map[string]string{ + "typ": "kb+jwt", + "alg": strings.ToUpper(alg), } - sdJwt.token = string(b) - hb, err := base64.RawURLEncoding.DecodeString(*token.Protected) - if err != nil { - return nil, &e.InvalidToken{Message: fmt.Sprintf("failed to decode protected header: %s", err.Error())} + kbBody := map[string]any{ + "iat": time.Now().Unix(), + "aud": aud, + "nonce": nonce, + "_sd_hash": "", //todo: calculate hash of sd-jwt } - var head map[string]any - err = json.Unmarshal(hb, &head) + + bKbHead, err := json.Marshal(kbHead) if err != nil { - return nil, &e.InvalidToken{Message: fmt.Sprintf("failed to json parse decoded protected header: %s", err.Error())} + return fmt.Errorf("error marshalling kb-jwt header: %w", err) } - sdJwt.head = head - sdJwt.signature = *token.Signature + b64KbHead := make([]byte, base64.RawURLEncoding.EncodedLen(len(bKbHead))) + base64.RawURLEncoding.Encode(b64KbHead, bKbHead) - disclosures, err := validateDisclosures(token.Disclosures) + bKbBody, err := json.Marshal(kbBody) if err != nil { - return nil, &e.InvalidToken{Message: fmt.Sprintf("failed to validate disclosures: %s", err.Error())} + return fmt.Errorf("error marshalling kb-jwt body: %w", err) } - sdJwt.disclosures = disclosures + b64KbBody := make([]byte, base64.RawURLEncoding.EncodedLen(len(bKbBody))) + base64.RawURLEncoding.Encode(b64KbBody, bKbBody) - b, err = base64.RawURLEncoding.DecodeString(*token.Payload) - if err != nil { - return nil, &e.InvalidToken{Message: fmt.Sprintf("failed to decode payload: %s", err.Error())} - } + signInput := string(b64KbHead) + "." + string(b64KbBody) - var m map[string]any - err = json.Unmarshal(b, &m) + sig, err := signer.Sign(rand.Reader, []byte(signInput), nil) if err != nil { - return nil, &e.InvalidToken{Message: fmt.Sprintf("failed to json parse decoded payload: %s", err.Error())} + return fmt.Errorf("error signing kb-jwt: %w", err) } - err = validateDigests(m) - if err != nil { - return nil, &e.InvalidToken{Message: fmt.Sprintf("failed to validate digests: %s", err.Error())} - } + b64Sig := make([]byte, base64.RawURLEncoding.EncodedLen(len(sig))) + base64.RawURLEncoding.Encode(b64Sig, sig) - sdJwt.body = m + kbJwt := signInput + "." + string(b64Sig) - valid, err := validateSignature(sdJwt.head, fmt.Sprintf("%s.%s", *token.Protected, *token.Payload), sdJwt.signature, publicKey) - if err != nil { - return nil, &e.InvalidToken{Message: fmt.Sprintf("failed to validate signature: %s", err.Error())} - } + s.kbJwt = &kbJwt + return nil +} + +// GetDisclosedClaims returns the claims that were disclosed in the token or included as plaintext values. +// This function will error one of the following scenarios is encountered: +// 1. The SD-JWT contains a disclosure that does not match an included digest +// 2. The SD-JWT contains a malformed _sd claim +// 3. The SD-JWT contains an unsupported value for the _sd_alg claim +// 4. The SD-JWT has a disclosure that is malformed for the use (e.g. doesn't contain a claim name for a non-array digest) +func (s *SdJwt) GetDisclosedClaims() (map[string]any, error) { + + disclosuresToCheck := make([]disclosure.Disclosure, len(s.disclosures)) + copy(disclosuresToCheck, s.disclosures) + + var h hash.Hash - if !valid { - return nil, &e.InvalidToken{Message: "invalid signature"} + strAlg, ok := s.body["_sd_alg"].(string) + if ok { + switch strings.ToLower(strAlg) { + case "sha-256", "": + // default to sha-256 + h = sha256.New() + case "sha-224": + h = sha256.New224() + case "sha-512": + h = sha512.New() + case "sha-384": + h = sha512.New384() + case "sha-512/224": + h = sha512.New512_224() + case "sha-512/256": + h = sha512.New512_256() + case "sha3-224": + h = crypto.SHA3_224.New() + case "sha3-256": + h = crypto.SHA3_256.New() + case "sha3-384": + h = crypto.SHA3_384.New() + case "sha3-512": + h = crypto.SHA3_512.New() + default: + return nil, errors.New("unsupported _sd_alg: " + s.body["_sd_alg"].(string)) + } + } else { + h = sha256.New() } - if sdJwt.kbJwt != nil { - valid, err = validateKbJwt(*sdJwt.kbJwt, sdJwt.body) + bodyMap := utils.CopyMap(*s.Body()) - if err != nil { - return nil, &e.InvalidToken{Message: fmt.Sprintf("failed to validate kb-jwt: %s", err.Error())} + for { + var indexesFound []int + for i := 0; i < len(disclosuresToCheck); i++ { + d := disclosuresToCheck[i] + + h.Write([]byte(d.EncodedValue)) + hashedDisclosures := h.Sum(nil) + base64HashedDisclosureBytes := make([]byte, base64.RawURLEncoding.EncodedLen(len(hashedDisclosures))) + base64.RawURLEncoding.Encode(base64HashedDisclosureBytes, hashedDisclosures) + + found, err := utils.ValidateSDClaims(utils.PointerMap(bodyMap), &d, string(base64HashedDisclosureBytes)) + if err != nil { + return nil, err + } + + if found { + indexesFound = append(indexesFound, i) + } + h.Reset() } - if !valid { - return nil, &e.InvalidToken{Message: "key-bound jwt has invalid signature"} + if len(indexesFound) == 0 { + return nil, fmt.Errorf("no matching digest found for: %v", utils.StringifyDisclosures(disclosuresToCheck)) + } + slices.Sort(indexesFound) + slices.Reverse(indexesFound) + for _, i := range indexesFound { + disclosuresToCheck = append(disclosuresToCheck[:i], disclosuresToCheck[i+1:]...) + } + if len(disclosuresToCheck) == 0 { + break } } - return sdJwt, nil + bodyMap = utils.StripSDClaims(bodyMap) + + return bodyMap, nil } -func validateJwt(token string, publicKey string) (*SdJwt, error) { +func validateJwt(token string) (*SdJwt, error) { sdJwt := &SdJwt{} - sdJwt.publicKey = publicKey - sections := strings.Split(token, "~") if len(sections) < 2 { - return nil, &e.InvalidToken{Message: "token has no specified disclosures"} + return nil, fmt.Errorf("%wtoken has no specified disclosures", e.InvalidToken) } - sdJwt.token = sections[0] - tokenSections := strings.Split(sections[0], ".") if len(tokenSections) != 3 { - return nil, &e.InvalidToken{Message: "token is not a valid JWT"} + return nil, fmt.Errorf("%wtoken is not a valid JWT", e.InvalidToken) } jwtHead := map[string]any{} hb, err := base64.RawURLEncoding.DecodeString(tokenSections[0]) if err != nil { - return nil, &e.InvalidToken{Message: fmt.Sprintf("failed to decode header: %s", err.Error())} + return nil, fmt.Errorf("%wfailed to decode header: %s", e.InvalidToken, err.Error()) } err = json.Unmarshal(hb, &jwtHead) if err != nil { - return nil, &e.InvalidToken{Message: fmt.Sprintf("failed to json parse decoded header: %s", err.Error())} + return nil, fmt.Errorf("%wfailed to json parse decoded header: %s", e.InvalidToken, err.Error()) } sdJwt.head = jwtHead @@ -182,486 +230,125 @@ func validateJwt(token string, publicKey string) (*SdJwt, error) { sdJwt.signature = tokenSections[2] if sections[len(sections)-1] != "" && sections[len(sections)-1][len(sections[len(sections)-1])-1:] != "~" { - kbJwt := checkForKbJwt(sections[len(sections)-1]) + kbJwt := utils.CheckForKbJwt(sections[len(sections)-1]) if kbJwt == nil { - return nil, &e.InvalidToken{Message: "if no kb-jwt is provided, the last disclosure must be followed by a ~"} + return nil, fmt.Errorf("%wif no kb-jwt is provided, the last disclosure must be followed by a ~", e.InvalidToken) } sdJwt.kbJwt = kbJwt sections = sections[:len(sections)-1] } - disclosures, err := validateDisclosures(sections[1:]) + disclosures, err := utils.ValidateDisclosures(sections[1:]) if err != nil { - return nil, &e.InvalidToken{Message: fmt.Sprintf("failed to validate disclosures: %s", err.Error())} + return nil, fmt.Errorf("%wfailed to validate disclosures: %s", e.InvalidToken, err.Error()) } sdJwt.disclosures = disclosures b, err := base64.RawURLEncoding.DecodeString(tokenSections[1]) if err != nil { - return nil, &e.InvalidToken{Message: fmt.Sprintf("failed to decode payload: %s", err.Error())} + return nil, fmt.Errorf("%wfailed to decode payload: %s", e.InvalidToken, err.Error()) } var m map[string]any err = json.Unmarshal(b, &m) if err != nil { - return nil, &e.InvalidToken{Message: fmt.Sprintf("failed to json parse decoded payload: %s", err.Error())} + return nil, fmt.Errorf("%wfailed to json parse decoded payload: %s", e.InvalidToken, err.Error()) } - err = validateDigests(m) + err = utils.ValidateDigests(m) if err != nil { - return nil, &e.InvalidToken{Message: fmt.Sprintf("failed to validate digests: %s", err.Error())} + return nil, fmt.Errorf("%wfailed to validate digests: %s", e.InvalidToken, err.Error()) } sdJwt.body = m - valid, err := validateSignature(sdJwt.head, strings.Join(tokenSections[0:2], "."), sdJwt.signature, publicKey) - if err != nil { - return nil, &e.InvalidToken{Message: fmt.Sprintf("failed to validate signature: %s", err.Error())} - } - - if !valid { - return nil, &e.InvalidToken{Message: "invalid signature"} - } - if sdJwt.kbJwt != nil { - valid, err = validateKbJwt(*sdJwt.kbJwt, sdJwt.body) + err = utils.ValidateKbJwt(*sdJwt.kbJwt, sdJwt.body) if err != nil { - return nil, &e.InvalidToken{Message: fmt.Sprintf("failed to validate kb-jwt: %s", err.Error())} - } - - if !valid { - return nil, &e.InvalidToken{Message: "key-bound jwt has invalid signature"} + return nil, fmt.Errorf("%wfailed to validate kb-jwt: %s", e.InvalidToken, err.Error()) } } return sdJwt, nil } -func validateKbJwt(kbJwt string, sdJwtBody map[string]any) (bool, error) { - kbjc := strings.Split(kbJwt, ".") - - if len(kbjc) != 3 { - return false, errors.New("kb jwt is in an invalid format") - } +func validateJws(token model.JwsSdJwt) (*SdJwt, error) { + sdJwt := &SdJwt{} - //head - kbhb, err := base64.RawURLEncoding.DecodeString(kbjc[0]) - if err != nil { - return false, err - } - var kbh map[string]any - err = json.Unmarshal(kbhb, &kbh) - if err != nil { - return false, err - } + sdJwt.kbJwt = token.KbJwt - //body - kbbb, err := base64.RawURLEncoding.DecodeString(kbjc[1]) + hb, err := base64.RawURLEncoding.DecodeString(*token.Protected) if err != nil { - return false, err + return nil, fmt.Errorf("%wfailed to decode protected header: %s", e.InvalidToken, err.Error()) } - var kbb map[string]any - err = json.Unmarshal(kbbb, &kbb) + var head map[string]any + err = json.Unmarshal(hb, &head) if err != nil { - return false, err + return nil, fmt.Errorf("%wfailed to json parse decoded protected header: %s", e.InvalidToken, err.Error()) } + sdJwt.head = head - //validate kb jwt contents - if kbh["typ"] != "kb+jwt" { - return false, errors.New("kb jwt is not of type kb+jwt") - } + sdJwt.signature = *token.Signature - //todo: if no other public key retrieval methods are specified, fall back to cnf - pkm := sdJwtBody["cnf"].(map[string]any)["jwk"].(map[string]any) - pkb, err := json.Marshal(pkm) + disclosures, err := utils.ValidateDisclosures(token.Disclosures) if err != nil { - return false, err - } - - return validateSignature(kbh, strings.Join(kbjc[0:2], "."), kbjc[2], string(pkb)) -} - -func checkForKbJwt(candidate string) *string { - if !strings.Contains(candidate, ".") { - return nil - } - - sections := strings.Split(candidate, ".") - if len(sections) != 3 { - return nil + return nil, fmt.Errorf("%wfailed to validate disclosures: %s", e.InvalidToken, err.Error()) } - return &candidate -} + sdJwt.disclosures = disclosures -func newDisclosure(d []byte) (*Disclosure, error) { - decodedDisclosure, err := base64.RawURLEncoding.DecodeString(string(d)) + b, err := base64.RawURLEncoding.DecodeString(*token.Payload) if err != nil { - return nil, err - } - if decodedDisclosure[0] != '[' || decodedDisclosure[len(decodedDisclosure)-1] != ']' { - return nil, errors.New("provided decoded disclosure is not a valid array") - } - - disclosure := &Disclosure{} - - parts := strings.Split(string(decodedDisclosure[1:len(decodedDisclosure)-1]), ",") - - disclosure.setRawValue(string(decodedDisclosure)) - disclosure.setEncodedValue(string(d)) - if len(parts) == 2 { - disclosure.setSalt(*cleanStr(parts[0])) - disclosure.setClaimValue(*cleanStr(parts[1])) - } else if strings.Contains(parts[1], "{") || strings.Contains(parts[1], "[") { - disclosure.setSalt(*cleanStr(parts[0])) - disclosure.setClaimValue(*cleanStr(strings.Join(parts[1:], ","))) - } else { - parts[2] = strings.Join(parts[2:], ",") - parts = parts[:3] - - if len(parts) != 3 { - return nil, errors.New("provided decoded disclosure does not have all required parts") - } - - disclosure.setSalt(*cleanStr(parts[0])) - disclosure.setClaimName(cleanStr(parts[1])) - disclosure.setClaimValue(*cleanStr(parts[2])) + return nil, fmt.Errorf("%wfailed to decode payload: %s", e.InvalidToken, err.Error()) } - return disclosure, nil -} - -func cleanStr(s string) *string { - return Pointer(strings.TrimSpace(strings.Trim(strings.TrimSpace(s), "\""))) -} - -func validateDisclosures(disclosures []string) ([]Disclosure, error) { - var disclosureArray []Disclosure - - if len(disclosures) == 0 { - return nil, errors.New("token has no specified disclosures") - } - - for _, d := range disclosures { - count := 0 - if d != "" { - for _, d2 := range disclosures { - if d == d2 { - count++ - } - } - if count > 1 { - return nil, errors.New("duplicate disclosure found") - } - dis, err := newDisclosure([]byte(d)) - if err != nil { - return nil, err - } - disclosureArray = append(disclosureArray, *dis) - } - } - return disclosureArray, nil -} - -func validateDigests(body map[string]any) error { - digests := getDigests(body) - - for _, d := range digests { - count := 0 - for _, d2 := range digests { - if d == d2 { - count++ - } - } - if count > 1 { - return errors.New("duplicate digest found") - } - } - return nil -} - -// GetDisclosedClaims returns the claims that were disclosed in the token or included as plaintext values. -// This function will error one of the following scenarios is encountered: -// 1. The SD-JWT contains a disclosure that does not match an included digest -// 2. The SD-JWT contains a malformed _sd claim -// 3. The SD-JWT contains an unsupported value for the _sd_alg claim -// 4. The SD-JWT has a disclosure that is malformed for the use (e.g. doesn't contain a claim name for a non-array digest) -func (s *SdJwt) GetDisclosedClaims() (map[string]any, error) { - - disclosuresToCheck := make([]Disclosure, len(s.disclosures)) - copy(disclosuresToCheck, s.disclosures) - - var h hash.Hash - - switch strings.ToLower(s.body["_sd_alg"].(string)) { - case "sha-256", "": - // default to sha-256 - h = sha256.New() - case "sha-224": - h = sha256.New224() - case "sha-512": - h = sha512.New() - case "sha-384": - h = sha512.New384() - case "sha-512/224": - h = sha512.New512_224() - case "sha-512/256": - h = sha512.New512_256() - case "sha3-224": - h = crypto.SHA3_224.New() - case "sha3-256": - h = crypto.SHA3_256.New() - case "sha3-384": - h = crypto.SHA3_384.New() - case "sha3-512": - h = crypto.SHA3_512.New() - default: - return nil, errors.New("unsupported _sd_alg: " + s.body["_sd_alg"].(string)) + var m map[string]any + err = json.Unmarshal(b, &m) + if err != nil { + return nil, fmt.Errorf("%wfailed to json parse decoded payload: %s", e.InvalidToken, err.Error()) } - for { - var indexesFound []int - for i := 0; i < len(disclosuresToCheck); i++ { - d := disclosuresToCheck[i] - - h.Write([]byte(d.EncodedValue())) - hashedDisclosures := h.Sum(nil) - base64HashedDisclosureBytes := make([]byte, base64.RawURLEncoding.EncodedLen(len(hashedDisclosures))) - base64.RawURLEncoding.Encode(base64HashedDisclosureBytes, hashedDisclosures) - - found, err := validateSDClaims(s.Body(), &d, string(base64HashedDisclosureBytes)) - if err != nil { - return nil, err - } - - if found { - indexesFound = append(indexesFound, i) - } - h.Reset() - } - - if len(indexesFound) == 0 { - return nil, fmt.Errorf("no matching digest found for: %v", stringifyDisclosures(disclosuresToCheck)) - } - slices.Sort(indexesFound) - slices.Reverse(indexesFound) - for _, i := range indexesFound { - disclosuresToCheck = append(disclosuresToCheck[:i], disclosuresToCheck[i+1:]...) - } - if len(disclosuresToCheck) == 0 { - break - } + err = utils.ValidateDigests(m) + if err != nil { + return nil, fmt.Errorf("%wfailed to validate digests: %s", e.InvalidToken, err.Error()) } - bodyMap := stripSDClaims(*s.Body()) + sdJwt.body = m - return bodyMap, nil -} + if sdJwt.kbJwt != nil { + err = utils.ValidateKbJwt(*sdJwt.kbJwt, sdJwt.body) -func stringifyDisclosures(disclosures []Disclosure) string { - result := "[" - for i, d := range disclosures { - if d.ClaimName() != nil { - result += "(" + *d.ClaimName() + ") " - } else { - result += " " - } - result += d.ClaimValue() + " " - if i != len(disclosures)-1 { - result += "," + if err != nil { + return nil, fmt.Errorf("%wfailed to validate kb-jwt: %s", e.InvalidToken, err.Error()) } } - result += "]" - return result -} -func stripSDClaims(body map[string]any) map[string]any { - bodyMap := make(map[string]any) - for k, v := range body { - switch reflect.TypeOf(v).Kind() { - case reflect.Map: - bodyMap[k] = stripSDClaims(v.(map[string]any)) - case reflect.Slice: - if k != "_sd" { - bodyMap[k] = stripSDClaimsFromSlice(v.([]any)) - } - default: - if k != "_sd_alg" { - bodyMap[k] = v - } - } - } - return bodyMap + return sdJwt, nil } -func stripSDClaimsFromSlice(input []any) []any { - for i, v := range input { - switch reflect.TypeOf(v).Kind() { - case reflect.Map: - input[i] = stripSDClaims(v.(map[string]any)) - case reflect.Slice: - input[i] = stripSDClaimsFromSlice(v.([]any)) - } - } - return input +// Body returns the body of the JWT +func (s *SdJwt) Body() *map[string]any { + return &s.body } -func validateSignature(head map[string]any, signedBody, signature string, publicKey string) (bool, error) { - alg := head["alg"].(string) - - signer, err := jose.GetSigner(strings.ToUpper(alg)) - if err != nil { - return false, err - } - - return signer.ValidateSignature(signedBody, signature, publicKey) +// Signature returns the signature of the provided token used to verify it +func (s *SdJwt) Signature() string { + return s.signature } -func getDigests(m map[string]any) []any { - var digests []any - for k, v := range m { - if reflect.TypeOf(v).Kind() == reflect.Map { - digests = append(digests, getDigests(v.(map[string]any))...) - } else if k == "_sd" { - digests = append(digests, v.([]any)...) - } else if reflect.TypeOf(v).Kind() == reflect.Slice { - for _, v2 := range v.([]any) { - b, err := json.Marshal(v2) - if err == nil { - var arrayDisclosure arrayDisclosure - err = json.Unmarshal(b, &arrayDisclosure) - if err == nil { - digests = append(digests, *arrayDisclosure.Digest) - } - } - } - } - } - return digests +// Head returns the head of the JWT +func (s *SdJwt) Head() map[string]any { + return s.head } -func parseClaimValue(cv string) (any, error) { - var m map[string]any - var s []any - var b bool - var i int - - err := json.Unmarshal([]byte(cv), &m) - if err == nil { - return m, nil - } - - err = json.Unmarshal([]byte(cv), &s) - if err == nil { - return s, nil - } - - err = json.Unmarshal([]byte(cv), &b) - if err == nil { - return b, nil - } - - err = json.Unmarshal([]byte(cv), &i) - if err == nil { - return i, nil - } - - //Return string as a fallback - return cv, nil +// Disclosures returns the disclosures of the SD-JWT +func (s *SdJwt) Disclosures() []disclosure.Disclosure { + return s.disclosures } -func validateSDClaims(values *map[string]any, currentDisclosure *Disclosure, base64HashedDisclosure string) (found bool, err error) { - if _, ok := (*values)["_sd"]; ok { - for _, digest := range (*values)["_sd"].([]any) { - sDigest := digest.(string) - if sDigest == base64HashedDisclosure { - if currentDisclosure.ClaimName() != nil { - val, err := parseClaimValue(currentDisclosure.ClaimValue()) - if err != nil { - return false, err - } - (*values)[*currentDisclosure.ClaimName()] = val - return true, nil - } else { - return false, errors.New("invalid disclosure format for _sd claim") - } - } - } - } - - for k, v := range *values { - if k != "_sd" && k != "_sd_alg" { - if reflect.TypeOf(v).Kind() == reflect.Slice { - found, err = validateArrayClaims(PointerSlice(v.([]any)), currentDisclosure, base64HashedDisclosure) - if err != nil { - return false, err - } - } else if reflect.TypeOf(v).Kind() == reflect.Map { - found, err = validateSDClaims(PointerMap(v.(map[string]any)), currentDisclosure, base64HashedDisclosure) - if err != nil { - return found, err - } - } - if found { - return true, nil - } - } - } - return false, nil -} - -func validateArrayClaims(s *[]any, currentDisclosure *Disclosure, base64HashedDisclosure string) (found bool, err error) { - - for i, v := range *s { - - switch reflect.TypeOf(v).Kind() { - - case reflect.Slice: - found, err = validateArrayClaims(PointerSlice(v.([]any)), currentDisclosure, base64HashedDisclosure) - if err != nil { - return false, err - } - if found { - return true, nil - } - - case reflect.Map: - ad := &arrayDisclosure{} - vb, err := json.Marshal(v) - if err != nil { - return false, err - } - - _ = json.Unmarshal(vb, ad) - - if ad.Digest != nil { - if *ad.Digest == base64HashedDisclosure { - if strings.Contains(currentDisclosure.ClaimValue(), "{") || strings.Contains(currentDisclosure.ClaimValue(), "[") { - var m map[string]any - err = json.Unmarshal([]byte(currentDisclosure.ClaimValue()), &m) - if err != nil { - return false, err - } - (*s)[i] = m - } else { - (*s)[i] = currentDisclosure.ClaimValue() - } - - return true, nil - } - } - - found, err = validateSDClaims(PointerMap(v.(map[string]any)), currentDisclosure, base64HashedDisclosure) - if err != nil { - return false, err - } - if found { - return true, nil - } - } - } - - return false, nil +// KbJwt returns the signed kb-jwt (if provided) +func (s *SdJwt) KbJwt() *string { + return s.kbJwt } diff --git a/sd-jwt_test.go b/sd-jwt_test.go index a2e475a..bf0c32c 100644 --- a/sd-jwt_test.go +++ b/sd-jwt_test.go @@ -1,8 +1,12 @@ package go_sd_jwt_test import ( + "crypto" + "crypto/rand" "encoding/json" "fmt" + "github.com/MichaelFraser99/go-jose" + "github.com/MichaelFraser99/go-jose/model" go_sd_jwt "github.com/MichaelFraser99/go-sd-jwt" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -14,8 +18,6 @@ func TestMain(m *testing.M) { os.Exit(m.Run()) } -var examplePublicKey = "{\"kty\":\"EC\",\"crv\":\"P-256\",\"x\":\"b28d4MwZMjw8-00CG4xfnn9SLMVMM19SlqZpVb_uNtQ\",\"y\":\"Xv5zWwuoaTgdS6hV43yI6gBwTnjukmFQQnJ_kCxzqk8\"}" - func TestFromToken(t *testing.T) { tests := []struct { name string @@ -27,14 +29,11 @@ func TestFromToken(t *testing.T) { token: "eyJhbGciOiAiRVMyNTYifQ.eyJfc2QiOiBbIkNyUWU3UzVrcUJBSHQtbk1ZWGdjNmJkdDJTSDVhVFkxc1VfTS1QZ2tqUEkiLCAiSnpZakg0c3ZsaUgwUjNQeUVNZmVadTZKdDY5dTVxZWhabzdGN0VQWWxTRSIsICJQb3JGYnBLdVZ1Nnh5bUphZ3ZrRnNGWEFiUm9jMkpHbEFVQTJCQTRvN2NJIiwgIlRHZjRvTGJnd2Q1SlFhSHlLVlFaVTlVZEdFMHc1cnREc3JaemZVYW9tTG8iLCAiWFFfM2tQS3QxWHlYN0tBTmtxVlI2eVoyVmE1TnJQSXZQWWJ5TXZSS0JNTSIsICJYekZyendzY002R242Q0pEYzZ2Vks4QmtNbmZHOHZPU0tmcFBJWmRBZmRFIiwgImdiT3NJNEVkcTJ4Mkt3LXc1d1BFemFrb2I5aFYxY1JEMEFUTjNvUUw5Sk0iLCAianN1OXlWdWx3UVFsaEZsTV8zSmx6TWFTRnpnbGhRRzBEcGZheVF3TFVLNCJdLCAiaXNzIjogImh0dHBzOi8vZXhhbXBsZS5jb20vaXNzdWVyIiwgImlhdCI6IDE2ODMwMDAwMDAsICJleHAiOiAxODgzMDAwMDAwLCAic3ViIjogInVzZXJfNDIiLCAibmF0aW9uYWxpdGllcyI6IFt7Ii4uLiI6ICJwRm5kamtaX1ZDem15VGE2VWpsWm8zZGgta284YUlLUWM5RGxHemhhVllvIn0sIHsiLi4uIjogIjdDZjZKa1B1ZHJ5M2xjYndIZ2VaOGtoQXYxVTFPU2xlclAwVmtCSnJXWjAifV0sICJfc2RfYWxnIjogInNoYS0yNTYiLCAiY25mIjogeyJqd2siOiB7Imt0eSI6ICJFQyIsICJjcnYiOiAiUC0yNTYiLCAieCI6ICJUQ0FFUjE5WnZ1M09IRjRqNFc0dmZTVm9ISVAxSUxpbERsczd2Q2VHZW1jIiwgInkiOiAiWnhqaVdXYlpNUUdIVldLVlE0aGJTSWlyc1ZmdWVjQ0U2dDRqVDlGMkhaUSJ9fX0.kmx687kUBiIDvKWgo2Dub-TpdCCRLZwtD7TOj4RoLsUbtFBI8sMrtH2BejXtm_P6fOAjKAVc_7LRNJFgm3PJhg~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgImdpdmVuX25hbWUiLCAiSm9obiJd~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImZhbWlseV9uYW1lIiwgIkRvZSJd~WyI2SWo3dE0tYTVpVlBHYm9TNXRtdlZBIiwgImVtYWlsIiwgImpvaG5kb2VAZXhhbXBsZS5jb20iXQ~WyJlSThaV205UW5LUHBOUGVOZW5IZGhRIiwgInBob25lX251bWJlciIsICIrMS0yMDItNTU1LTAxMDEiXQ~WyJRZ19PNjR6cUF4ZTQxMmExMDhpcm9BIiwgInBob25lX251bWJlcl92ZXJpZmllZCIsIHRydWVd~WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0~WyJQYzMzSk0yTGNoY1VfbEhnZ3ZfdWZRIiwgImJpcnRoZGF0ZSIsICIxOTQwLTAxLTAxIl0~WyJHMDJOU3JRZmpGWFE3SW8wOXN5YWpBIiwgInVwZGF0ZWRfYXQiLCAxNTcwMDAwMDAwXQ~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgIlVTIl0~WyJuUHVvUW5rUkZxM0JJZUFtN0FuWEZBIiwgIkRFIl0~", validate: func(t *testing.T, sdJwt *go_sd_jwt.SdJwt, err error) { if err != nil { - t.Error("error should be nil", err) + t.Errorf("error should be nil: %s", err.Error()) } if sdJwt == nil { t.Error("sdJwt should not be nil") } - if sdJwt.Token() == "" { - t.Error("token should not be empty") - } if sdJwt.Head() == nil || len(sdJwt.Head()) == 0 { t.Error("head should not be empty") } @@ -62,7 +61,7 @@ func TestFromToken(t *testing.T) { assert.Nil(t, claims["_sd"]) assert.Nil(t, claims["_sd_alg"]) - assert.Equal(t, 1570000000, claims["updated_at"]) + assert.Equal(t, float64(1570000000), claims["updated_at"]) assert.Len(t, claims["nationalities"], 2) assert.Contains(t, claims["nationalities"], "DE") assert.Contains(t, claims["nationalities"], "US") @@ -90,9 +89,6 @@ func TestFromToken(t *testing.T) { if sdJwt == nil { t.Error("sdJwt should not be nil") } - if sdJwt.Token() == "" { - t.Error("token should not be empty") - } if sdJwt.Head() == nil || len(sdJwt.Head()) == 0 { t.Error("head should not be empty") } @@ -126,7 +122,7 @@ func TestFromToken(t *testing.T) { assert.NotNil(t, claims["address"]) assert.Nil(t, claims["address"].(map[string]any)["_sd"]) assert.Equal(t, "JP", claims["address"].(map[string]any)["country"]) - assert.Equal(t, "\\u6e2f\\u533a", claims["address"].(map[string]any)["region"]) + assert.Equal(t, "港区", claims["address"].(map[string]any)["region"]) }, }, { @@ -139,9 +135,6 @@ func TestFromToken(t *testing.T) { if sdJwt == nil { t.Error("sdJwt should not be nil") } - if sdJwt.Token() == "" { - t.Error("token should not be empty") - } if sdJwt.Head() == nil || len(sdJwt.Head()) == 0 { t.Error("head should not be empty") } @@ -191,7 +184,7 @@ func TestFromToken(t *testing.T) { assert.NotNil(t, claims["verified_claims"].(map[string]any)["claims"].(map[string]any)["given_name"]) assert.Equal(t, "Max", claims["verified_claims"].(map[string]any)["claims"].(map[string]any)["given_name"]) assert.NotNil(t, claims["verified_claims"].(map[string]any)["claims"].(map[string]any)["family_name"]) - assert.Equal(t, "M\\u00fcller", claims["verified_claims"].(map[string]any)["claims"].(map[string]any)["family_name"]) + assert.Equal(t, "Müller", claims["verified_claims"].(map[string]any)["claims"].(map[string]any)["family_name"]) assert.NotNil(t, claims["verified_claims"].(map[string]any)["claims"].(map[string]any)["address"]) assert.Equal(t, 4, len(claims["verified_claims"].(map[string]any)["claims"].(map[string]any)["address"].(map[string]any))) assert.NotNil(t, claims["verified_claims"].(map[string]any)["claims"].(map[string]any)["address"].(map[string]any)["locality"]) @@ -214,9 +207,6 @@ func TestFromToken(t *testing.T) { if sdJwt == nil { t.Error("sdJwt should not be nil") } - if sdJwt.Token() == "" { - t.Error("token should not be empty") - } if sdJwt.Head() == nil || len(sdJwt.Head()) == 0 { t.Error("head should not be empty") } @@ -253,41 +243,6 @@ func TestFromToken(t *testing.T) { assert.Equal(t, "PersonIdentificationData", claims["type"]) }, }, - { - name: "valid jws token", - token: "{\"payload\": \"eyJfc2QiOiBbIjRIQm42YUlZM1d0dUdHV1R4LXFVajZjZGs2V0JwWnlnbHRkRmF2UGE3TFkiLCAiOHNtMVFDZjAyMXBObkhBQ0k1c1A0bTRLWmd5Tk9PQVljVGo5SE5hQzF3WSIsICJTRE43OU5McEFuSFBta3JkZVlkRWE4OVhaZHNrME04REtZU1FPVTJaeFFjIiwgIlh6RnJ6d3NjTTZHbjZDSkRjNnZWSzhCa01uZkc4dk9TS2ZwUElaZEFmZEUiLCAiZ2JPc0k0RWRxMngyS3ctdzV3UEV6YWtvYjloVjFjUkQwQVROM29RTDlKTSIsICJqTUNYVnotLTliOHgzN1ljb0RmWFFpbnp3MXdaY2NjZkZSQkNGR3FkRzJvIiwgIm9LSTFHZDJmd041V3d2amxGa29oaWRHdmltLTMxT3VsUjNxMGhyRE8wNzgiXSwgImlzcyI6ICJodHRwczovL2V4YW1wbGUuY29tL2lzc3VlciIsICJpYXQiOiAxNjgzMDAwMDAwLCAiZXhwIjogMTg4MzAwMDAwMCwgIl9zZF9hbGciOiAic2hhLTI1NiIsICJjbmYiOiB7Imp3ayI6IHsia3R5IjogIkVDIiwgImNydiI6ICJQLTI1NiIsICJ4IjogIlRDQUVSMTladnUzT0hGNGo0VzR2ZlNWb0hJUDFJTGlsRGxzN3ZDZUdlbWMiLCAieSI6ICJaeGppV1diWk1RR0hWV0tWUTRoYlNJaXJzVmZ1ZWNDRTZ0NGpUOUYySFpRIn19fQ\",\"protected\": \"eyJhbGciOiAiRVMyNTYifQ\",\"signature\": \"qNNvkravlssjHS8TSnj5lAFc5on6MjG0peMt8Zjh1Yefxn0DxkcVOU9r7t1VNehJISOFL7NuJ5V27DVbNJBLoA\",\"disclosures\": [\"WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgInN1YiIsICJqb2huX2RvZV80MiJd\",\"WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImdpdmVuX25hbWUiLCAiSm9obiJd\",\"WyI2SWo3dE0tYTVpVlBHYm9TNXRtdlZBIiwgImZhbWlseV9uYW1lIiwgIkRvZSJd\",\"WyJlSThaV205UW5LUHBOUGVOZW5IZGhRIiwgImVtYWlsIiwgImpvaG5kb2VAZXhhbXBsZS5jb20iXQ\",\"WyJRZ19PNjR6cUF4ZTQxMmExMDhpcm9BIiwgInBob25lX251bWJlciIsICIrMS0yMDItNTU1LTAxMDEiXQ\",\"WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0\",\"WyJQYzMzSk0yTGNoY1VfbEhnZ3ZfdWZRIiwgImJpcnRoZGF0ZSIsICIxOTQwLTAxLTAxIl0\"]}", - validate: func(t *testing.T, sdJwt *go_sd_jwt.SdJwt, err error) { - require.NoError(t, err) - require.NotNil(t, sdJwt) - assert.NotEmpty(t, sdJwt.Token()) - assert.NotEmpty(t, sdJwt.Head()) - assert.NotEmpty(t, sdJwt.Body()) - assert.NotEmpty(t, sdJwt.Signature()) - assert.NotEmpty(t, sdJwt.Disclosures()) - assert.Len(t, sdJwt.Disclosures(), 7) - assert.Nil(t, sdJwt.KbJwt()) - - claims, err := sdJwt.GetDisclosedClaims() - require.NoError(t, err) - - b, _ := json.Marshal(claims) - t.Log(string(b)) - - assert.Nil(t, claims["_sd"]) - assert.Nil(t, claims["_sd_alg"]) - assert.Equal(t, "1940-01-01", claims["birthdate"]) - assert.NotNil(t, claims["address"]) - assert.Equal(t, "123 Main St", claims["address"].(map[string]any)["street_address"]) - assert.Equal(t, "Anytown", claims["address"].(map[string]any)["locality"]) - assert.Equal(t, "Anystate", claims["address"].(map[string]any)["region"]) - assert.Equal(t, "US", claims["address"].(map[string]any)["country"]) - assert.Equal(t, "+1-202-555-0101", claims["phone_number"]) - assert.Equal(t, "johndoe@example.com", claims["email"]) - assert.Equal(t, "John", claims["given_name"]) - assert.Equal(t, "Doe", claims["family_name"]) - assert.Equal(t, "john_doe_42", claims["sub"]) - }, - }, { name: "valid token but duplicate disclosure", token: "eyJhbGciOiAiRVMyNTYifQ.eyJfc2QiOiBbIkNyUWU3UzVrcUJBSHQtbk1ZWGdjNmJkdDJTSDVhVFkxc1VfTS1QZ2tqUEkiLCAiSnpZakg0c3ZsaUgwUjNQeUVNZmVadTZKdDY5dTVxZWhabzdGN0VQWWxTRSIsICJQb3JGYnBLdVZ1Nnh5bUphZ3ZrRnNGWEFiUm9jMkpHbEFVQTJCQTRvN2NJIiwgIlRHZjRvTGJnd2Q1SlFhSHlLVlFaVTlVZEdFMHc1cnREc3JaemZVYW9tTG8iLCAiWFFfM2tQS3QxWHlYN0tBTmtxVlI2eVoyVmE1TnJQSXZQWWJ5TXZSS0JNTSIsICJYekZyendzY002R242Q0pEYzZ2Vks4QmtNbmZHOHZPU0tmcFBJWmRBZmRFIiwgImdiT3NJNEVkcTJ4Mkt3LXc1d1BFemFrb2I5aFYxY1JEMEFUTjNvUUw5Sk0iLCAianN1OXlWdWx3UVFsaEZsTV8zSmx6TWFTRnpnbGhRRzBEcGZheVF3TFVLNCJdLCAiaXNzIjogImh0dHBzOi8vZXhhbXBsZS5jb20vaXNzdWVyIiwgImlhdCI6IDE2ODMwMDAwMDAsICJleHAiOiAxODgzMDAwMDAwLCAic3ViIjogInVzZXJfNDIiLCAibmF0aW9uYWxpdGllcyI6IFt7Ii4uLiI6ICJwRm5kamtaX1ZDem15VGE2VWpsWm8zZGgta284YUlLUWM5RGxHemhhVllvIn0sIHsiLi4uIjogIjdDZjZKa1B1ZHJ5M2xjYndIZ2VaOGtoQXYxVTFPU2xlclAwVmtCSnJXWjAifV0sICJfc2RfYWxnIjogInNoYS0yNTYiLCAiY25mIjogeyJqd2siOiB7Imt0eSI6ICJFQyIsICJjcnYiOiAiUC0yNTYiLCAieCI6ICJUQ0FFUjE5WnZ1M09IRjRqNFc0dmZTVm9ISVAxSUxpbERsczd2Q2VHZW1jIiwgInkiOiAiWnhqaVdXYlpNUUdIVldLVlE0aGJTSWlyc1ZmdWVjQ0U2dDRqVDlGMkhaUSJ9fX0.kmx687kUBiIDvKWgo2Dub-TpdCCRLZwtD7TOj4RoLsUbtFBI8sMrtH2BejXtm_P6fOAjKAVc_7LRNJFgm3PJhg~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgImdpdmVuX25hbWUiLCAiSm9obiJd~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImZhbWlseV9uYW1lIiwgIkRvZSJd~WyI2SWo3dE0tYTVpVlBHYm9TNXRtdlZBIiwgImVtYWlsIiwgImpvaG5kb2VAZXhhbXBsZS5jb20iXQ~WyJlSThaV205UW5LUHBOUGVOZW5IZGhRIiwgInBob25lX251bWJlciIsICIrMS0yMDItNTU1LTAxMDEiXQ~WyJRZ19PNjR6cUF4ZTQxMmExMDhpcm9BIiwgInBob25lX251bWJlcl92ZXJpZmllZCIsIHRydWVd~WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0~WyJQYzMzSk0yTGNoY1VfbEhnZ3ZfdWZRIiwgImJpcnRoZGF0ZSIsICIxOTQwLTAxLTAxIl0~WyJHMDJOU3JRZmpGWFE3SW8wOXN5YWpBIiwgInVwZGF0ZWRfYXQiLCAxNTcwMDAwMDAwXQ~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgIlVTIl0~WyJuUHVvUW5rUkZxM0JJZUFtN0FuWEZBIiwgIkRFIl0~WyJQYzMzSk0yTGNoY1VfbEhnZ3ZfdWZRIiwgImJpcnRoZGF0ZSIsICIxOTQwLTAxLTAxIl0~", @@ -321,12 +276,78 @@ func TestFromToken(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - sdJwt, err := go_sd_jwt.FromToken(tt.token, examplePublicKey) + sdJwt, err := go_sd_jwt.New(tt.token) tt.validate(t, sdJwt, err) }) } } +func TestNewFromJws(t *testing.T) { + token := "{\"payload\": \"eyJfc2QiOiBbIjRIQm42YUlZM1d0dUdHV1R4LXFVajZjZGs2V0JwWnlnbHRkRmF2UGE3TFkiLCAiOHNtMVFDZjAyMXBObkhBQ0k1c1A0bTRLWmd5Tk9PQVljVGo5SE5hQzF3WSIsICJTRE43OU5McEFuSFBta3JkZVlkRWE4OVhaZHNrME04REtZU1FPVTJaeFFjIiwgIlh6RnJ6d3NjTTZHbjZDSkRjNnZWSzhCa01uZkc4dk9TS2ZwUElaZEFmZEUiLCAiZ2JPc0k0RWRxMngyS3ctdzV3UEV6YWtvYjloVjFjUkQwQVROM29RTDlKTSIsICJqTUNYVnotLTliOHgzN1ljb0RmWFFpbnp3MXdaY2NjZkZSQkNGR3FkRzJvIiwgIm9LSTFHZDJmd041V3d2amxGa29oaWRHdmltLTMxT3VsUjNxMGhyRE8wNzgiXSwgImlzcyI6ICJodHRwczovL2V4YW1wbGUuY29tL2lzc3VlciIsICJpYXQiOiAxNjgzMDAwMDAwLCAiZXhwIjogMTg4MzAwMDAwMCwgIl9zZF9hbGciOiAic2hhLTI1NiIsICJjbmYiOiB7Imp3ayI6IHsia3R5IjogIkVDIiwgImNydiI6ICJQLTI1NiIsICJ4IjogIlRDQUVSMTladnUzT0hGNGo0VzR2ZlNWb0hJUDFJTGlsRGxzN3ZDZUdlbWMiLCAieSI6ICJaeGppV1diWk1RR0hWV0tWUTRoYlNJaXJzVmZ1ZWNDRTZ0NGpUOUYySFpRIn19fQ\",\"protected\": \"eyJhbGciOiAiRVMyNTYifQ\",\"signature\": \"qNNvkravlssjHS8TSnj5lAFc5on6MjG0peMt8Zjh1Yefxn0DxkcVOU9r7t1VNehJISOFL7NuJ5V27DVbNJBLoA\",\"disclosures\": [\"WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgInN1YiIsICJqb2huX2RvZV80MiJd\",\"WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImdpdmVuX25hbWUiLCAiSm9obiJd\",\"WyI2SWo3dE0tYTVpVlBHYm9TNXRtdlZBIiwgImZhbWlseV9uYW1lIiwgIkRvZSJd\",\"WyJlSThaV205UW5LUHBOUGVOZW5IZGhRIiwgImVtYWlsIiwgImpvaG5kb2VAZXhhbXBsZS5jb20iXQ\",\"WyJRZ19PNjR6cUF4ZTQxMmExMDhpcm9BIiwgInBob25lX251bWJlciIsICIrMS0yMDItNTU1LTAxMDEiXQ\",\"WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0\",\"WyJQYzMzSk0yTGNoY1VfbEhnZ3ZfdWZRIiwgImJpcnRoZGF0ZSIsICIxOTQwLTAxLTAxIl0\"]}" + + sdJwt, err := go_sd_jwt.NewFromJws(token) + + require.NoError(t, err) + require.NotNil(t, sdJwt) + assert.NotEmpty(t, sdJwt.Head()) + assert.NotEmpty(t, sdJwt.Body()) + assert.NotEmpty(t, sdJwt.Signature()) + assert.NotEmpty(t, sdJwt.Disclosures()) + assert.Len(t, sdJwt.Disclosures(), 7) + assert.Nil(t, sdJwt.KbJwt()) + + claims, err := sdJwt.GetDisclosedClaims() + require.NoError(t, err) + + b, _ := json.Marshal(claims) + t.Log(string(b)) + + assert.Nil(t, claims["_sd"]) + assert.Nil(t, claims["_sd_alg"]) + assert.Equal(t, "1940-01-01", claims["birthdate"]) + assert.NotNil(t, claims["address"]) + assert.Equal(t, "123 Main St", claims["address"].(map[string]any)["street_address"]) + assert.Equal(t, "Anytown", claims["address"].(map[string]any)["locality"]) + assert.Equal(t, "Anystate", claims["address"].(map[string]any)["region"]) + assert.Equal(t, "US", claims["address"].(map[string]any)["country"]) + assert.Equal(t, "+1-202-555-0101", claims["phone_number"]) + assert.Equal(t, "johndoe@example.com", claims["email"]) + assert.Equal(t, "John", claims["given_name"]) + assert.Equal(t, "Doe", claims["family_name"]) + assert.Equal(t, "john_doe_42", claims["sub"]) +} + +func TestSdJwt_AddKeyBindingJwt(t *testing.T) { + token := "eyJhbGciOiAiRVMyNTYifQ.eyJfc2QiOiBbIi1hU3puSWQ5bVdNOG9jdVFvbENsbHN4VmdncTEtdkhXNE90bmhVdFZtV3ciLCAiSUticllObjN2QTdXRUZyeXN2YmRCSmpERFVfRXZRSXIwVzE4dlRScFVTZyIsICJvdGt4dVQxNG5CaXd6TkozTVBhT2l0T2w5cFZuWE9hRUhhbF94a3lOZktJIl0sICJpc3MiOiAiaHR0cHM6Ly9pc3N1ZXIuZXhhbXBsZS5jb20iLCAiaWF0IjogMTY4MzAwMDAwMCwgImV4cCI6IDE4ODMwMDAwMDAsICJ2ZXJpZmllZF9jbGFpbXMiOiB7InZlcmlmaWNhdGlvbiI6IHsiX3NkIjogWyI3aDRVRTlxU2N2REtvZFhWQ3VvS2ZLQkpwVkJmWE1GX1RtQUdWYVplM1NjIiwgInZUd2UzcmFISUZZZ0ZBM3hhVUQyYU14Rno1b0RvOGlCdTA1cUtsT2c5THciXSwgInRydXN0X2ZyYW1ld29yayI6ICJkZV9hbWwiLCAiZXZpZGVuY2UiOiBbeyIuLi4iOiAidFlKMFREdWN5WlpDUk1iUk9HNHFSTzV2a1BTRlJ4RmhVRUxjMThDU2wzayJ9XX0sICJjbGFpbXMiOiB7Il9zZCI6IFsiUmlPaUNuNl93NVpIYWFka1FNcmNRSmYwSnRlNVJ3dXJSczU0MjMxRFRsbyIsICJTXzQ5OGJicEt6QjZFYW5mdHNzMHhjN2NPYW9uZVJyM3BLcjdOZFJtc01vIiwgIldOQS1VTks3Rl96aHNBYjlzeVdPNklJUTF1SGxUbU9VOHI4Q3ZKMGNJTWsiLCAiV3hoX3NWM2lSSDliZ3JUQkppLWFZSE5DTHQtdmpoWDFzZC1pZ09mXzlsayIsICJfTy13SmlIM2VuU0I0Uk9IbnRUb1FUOEptTHR6LW1oTzJmMWM4OVhvZXJRIiwgImh2RFhod21HY0pRc0JDQTJPdGp1TEFjd0FNcERzYVUwbmtvdmNLT3FXTkUiXX19LCAiX3NkX2FsZyI6ICJzaGEtMjU2In0.kbfpTas9_-dLMgyeUxIXuBGLtCZUO2bG9JA7v73ebzpX1LA5MBtQsyZZut-Bm3_TW8sTqLCDPUN4ZC5pKCyQig~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgInRpbWUiLCAiMjAxMi0wNC0yM1QxODoyNVoiXQ~WyJQYzMzSk0yTGNoY1VfbEhnZ3ZfdWZRIiwgeyJfc2QiOiBbIjl3cGpWUFd1RDdQSzBuc1FETDhCMDZsbWRnVjNMVnliaEh5ZFFwVE55TEkiLCAiRzVFbmhPQU9vVTlYXzZRTU52ekZYanBFQV9SYy1BRXRtMWJHX3djYUtJayIsICJJaHdGcldVQjYzUmNacTl5dmdaMFhQYzdHb3doM08ya3FYZUJJc3dnMUI0IiwgIldweFE0SFNvRXRjVG1DQ0tPZURzbEJfZW11Y1lMejJvTzhvSE5yMWJFVlEiXX1d~WyJlSThaV205UW5LUHBOUGVOZW5IZGhRIiwgIm1ldGhvZCIsICJwaXBwIl0~WyJHMDJOU3JRZmpGWFE3SW8wOXN5YWpBIiwgImdpdmVuX25hbWUiLCAiTWF4Il0~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgImZhbWlseV9uYW1lIiwgIk1cdTAwZmNsbGVyIl0~WyJ5MXNWVTV3ZGZKYWhWZGd3UGdTN1JRIiwgImFkZHJlc3MiLCB7ImxvY2FsaXR5IjogIk1heHN0YWR0IiwgInBvc3RhbF9jb2RlIjogIjEyMzQ0IiwgImNvdW50cnkiOiAiREUiLCAic3RyZWV0X2FkZHJlc3MiOiAiV2VpZGVuc3RyYVx1MDBkZmUgMjIifV0~" + sdJwt, err := go_sd_jwt.New(token) + if err != nil { + t.Fatalf("no error should be thrown: %s", err.Error()) + } + + if sdJwt.KbJwt() != nil { + t.Fatalf("no kb jwt should yet exist") + } + + signer, err := jose.GetSigner(model.RS256, &model.Opts{BitSize: 2048}) + if err != nil { + t.Fatalf("failed to get signer %s", err.Error()) + } + nonce := make([]byte, 32) + _, err = rand.Read(nonce) + if err != nil { + t.Errorf("error generating nonce value: %s", err.Error()) + } + + err = sdJwt.AddKeyBindingJwt(signer, crypto.SHA256, signer.Alg().String(), "https://unused.com", string(nonce)) + if err != nil { + t.Errorf("no error should be thrown: %s", err.Error()) + } + + if sdJwt.KbJwt() == nil { + t.Error("KB Jwt should now exist") + } +} + func TestFromToken_AllDuplicateDigestScenarios(t *testing.T) { duplicateDigestSdClaimJwt := "eyJhbGciOiAiRVMyNTYifQ.ew0KICAiX3NkIjogWw0KICAgICJDclFlN1M1a3FCQUh0LW5NWVhnYzZiZHQyU0g1YVRZMXNVX00tUGdralBJIiwNCiAgICAiSnpZakg0c3ZsaUgwUjNQeUVNZmVadTZKdDY5dTVxZWhabzdGN0VQWWxTRSIsDQogICAgIlBvckZicEt1VnU2eHltSmFndmtGc0ZYQWJSb2MySkdsQVVBMkJBNG83Y0kiLA0KICAgICJUR2Y0b0xiZ3dkNUpRYUh5S1ZRWlU5VWRHRTB3NXJ0RHNyWnpmVWFvbUxvIiwNCiAgICAiWFFfM2tQS3QxWHlYN0tBTmtxVlI2eVoyVmE1TnJQSXZQWWJ5TXZSS0JNTSIsDQogICAgIlh6RnJ6d3NjTTZHbjZDSkRjNnZWSzhCa01uZkc4dk9TS2ZwUElaZEFmZEUiLA0KICAgICJnYk9zSTRFZHEyeDJLdy13NXdQRXpha29iOWhWMWNSRDBBVE4zb1FMOUpNIiwNCiAgICAianN1OXlWdWx3UVFsaEZsTV8zSmx6TWFTRnpnbGhRRzBEcGZheVF3TFVLNCIsDQogICAgImpzdTl5VnVsd1FRbGhGbE1fM0psek1hU0Z6Z2xoUUcwRHBmYXlRd0xVSzQiDQogIF0sDQogICJpc3MiOiAiaHR0cHM6Ly9leGFtcGxlLmNvbS9pc3N1ZXIiLA0KICAiaWF0IjogMTY4MzAwMDAwMCwNCiAgImV4cCI6IDE4ODMwMDAwMDAsDQogICJzdWIiOiAidXNlcl80MiIsDQogICJuYXRpb25hbGl0aWVzIjogWw0KICAgIHsNCiAgICAgICIuLi4iOiAicEZuZGprWl9WQ3pteVRhNlVqbFpvM2RoLWtvOGFJS1FjOURsR3poYVZZbyINCiAgICB9LA0KICAgIHsNCiAgICAgICIuLi4iOiAiN0NmNkprUHVkcnkzbGNid0hnZVo4a2hBdjFVMU9TbGVyUDBWa0JKcldaMCINCiAgICB9DQogIF0sDQogICJfc2RfYWxnIjogInNoYS0yNTYiLA0KICAiY25mIjogew0KICAgICJqd2siOiB7DQogICAgICAia3R5IjogIkVDIiwNCiAgICAgICJjcnYiOiAiUC0yNTYiLA0KICAgICAgIngiOiAiVENBRVIxOVp2dTNPSEY0ajRXNHZmU1ZvSElQMUlMaWxEbHM3dkNlR2VtYyIsDQogICAgICAieSI6ICJaeGppV1diWk1RR0hWV0tWUTRoYlNJaXJzVmZ1ZWNDRTZ0NGpUOUYySFpRIg0KICAgIH0NCiAgfQ0KfQ.kmx687kUBiIDvKWgo2Dub-TpdCCRLZwtD7TOj4RoLsUbtFBI8sMrtH2BejXtm_P6fOAjKAVc_7LRNJFgm3PJhg~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgImdpdmVuX25hbWUiLCAiSm9obiJd~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImZhbWlseV9uYW1lIiwgIkRvZSJd~WyI2SWo3dE0tYTVpVlBHYm9TNXRtdlZBIiwgImVtYWlsIiwgImpvaG5kb2VAZXhhbXBsZS5jb20iXQ~WyJlSThaV205UW5LUHBOUGVOZW5IZGhRIiwgInBob25lX251bWJlciIsICIrMS0yMDItNTU1LTAxMDEiXQ~WyJRZ19PNjR6cUF4ZTQxMmExMDhpcm9BIiwgInBob25lX251bWJlcl92ZXJpZmllZCIsIHRydWVd~WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0~WyJQYzMzSk0yTGNoY1VfbEhnZ3ZfdWZRIiwgImJpcnRoZGF0ZSIsICIxOTQwLTAxLTAxIl0~WyJHMDJOU3JRZmpGWFE3SW8wOXN5YWpBIiwgInVwZGF0ZWRfYXQiLCAxNTcwMDAwMDAwXQ~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgIlVTIl0~WyJuUHVvUW5rUkZxM0JJZUFtN0FuWEZBIiwgIkRFIl0~" duplicateDigestArrayClaimJwt := "eyJhbGciOiAiRVMyNTYifQ.ew0KICAiX3NkIjogWw0KICAgICJDclFlN1M1a3FCQUh0LW5NWVhnYzZiZHQyU0g1YVRZMXNVX00tUGdralBJIiwNCiAgICAiSnpZakg0c3ZsaUgwUjNQeUVNZmVadTZKdDY5dTVxZWhabzdGN0VQWWxTRSIsDQogICAgIlBvckZicEt1VnU2eHltSmFndmtGc0ZYQWJSb2MySkdsQVVBMkJBNG83Y0kiLA0KICAgICJUR2Y0b0xiZ3dkNUpRYUh5S1ZRWlU5VWRHRTB3NXJ0RHNyWnpmVWFvbUxvIiwNCiAgICAiWFFfM2tQS3QxWHlYN0tBTmtxVlI2eVoyVmE1TnJQSXZQWWJ5TXZSS0JNTSIsDQogICAgIlh6RnJ6d3NjTTZHbjZDSkRjNnZWSzhCa01uZkc4dk9TS2ZwUElaZEFmZEUiLA0KICAgICJnYk9zSTRFZHEyeDJLdy13NXdQRXpha29iOWhWMWNSRDBBVE4zb1FMOUpNIiwNCiAgICAianN1OXlWdWx3UVFsaEZsTV8zSmx6TWFTRnpnbGhRRzBEcGZheVF3TFVLNCINCiAgXSwNCiAgImlzcyI6ICJodHRwczovL2V4YW1wbGUuY29tL2lzc3VlciIsDQogICJpYXQiOiAxNjgzMDAwMDAwLA0KICAiZXhwIjogMTg4MzAwMDAwMCwNCiAgInN1YiI6ICJ1c2VyXzQyIiwNCiAgIm5hdGlvbmFsaXRpZXMiOiBbDQogICAgew0KICAgICAgIi4uLiI6ICJwRm5kamtaX1ZDem15VGE2VWpsWm8zZGgta284YUlLUWM5RGxHemhhVllvIg0KICAgIH0sDQogICAgew0KICAgICAgIi4uLiI6ICI3Q2Y2SmtQdWRyeTNsY2J3SGdlWjhraEF2MVUxT1NsZXJQMFZrQkpyV1owIg0KICAgIH0sDQogICAgew0KICAgICAgIi4uLiI6ICI3Q2Y2SmtQdWRyeTNsY2J3SGdlWjhraEF2MVUxT1NsZXJQMFZrQkpyV1owIg0KICAgIH0NCiAgXSwNCiAgIl9zZF9hbGciOiAic2hhLTI1NiIsDQogICJjbmYiOiB7DQogICAgImp3ayI6IHsNCiAgICAgICJrdHkiOiAiRUMiLA0KICAgICAgImNydiI6ICJQLTI1NiIsDQogICAgICAieCI6ICJUQ0FFUjE5WnZ1M09IRjRqNFc0dmZTVm9ISVAxSUxpbERsczd2Q2VHZW1jIiwNCiAgICAgICJ5IjogIlp4amlXV2JaTVFHSFZXS1ZRNGhiU0lpcnNWZnVlY0NFNnQ0alQ5RjJIWlEiDQogICAgfQ0KICB9DQp9.kmx687kUBiIDvKWgo2Dub-TpdCCRLZwtD7TOj4RoLsUbtFBI8sMrtH2BejXtm_P6fOAjKAVc_7LRNJFgm3PJhg~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgImdpdmVuX25hbWUiLCAiSm9obiJd~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImZhbWlseV9uYW1lIiwgIkRvZSJd~WyI2SWo3dE0tYTVpVlBHYm9TNXRtdlZBIiwgImVtYWlsIiwgImpvaG5kb2VAZXhhbXBsZS5jb20iXQ~WyJlSThaV205UW5LUHBOUGVOZW5IZGhRIiwgInBob25lX251bWJlciIsICIrMS0yMDItNTU1LTAxMDEiXQ~WyJRZ19PNjR6cUF4ZTQxMmExMDhpcm9BIiwgInBob25lX251bWJlcl92ZXJpZmllZCIsIHRydWVd~WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0~WyJQYzMzSk0yTGNoY1VfbEhnZ3ZfdWZRIiwgImJpcnRoZGF0ZSIsICIxOTQwLTAxLTAxIl0~WyJHMDJOU3JRZmpGWFE3SW8wOXN5YWpBIiwgInVwZGF0ZWRfYXQiLCAxNTcwMDAwMDAwXQ~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgIlVTIl0~WyJuUHVvUW5rUkZxM0JJZUFtN0FuWEZBIiwgIkRFIl0~" @@ -334,7 +355,7 @@ func TestFromToken_AllDuplicateDigestScenarios(t *testing.T) { duplicateDigestNestedSdClaimJwt := "eyJhbGciOiAiRVMyNTYifQ.ew0KICAiX3NkIjogWw0KICAgICJDclFlN1M1a3FCQUh0LW5NWVhnYzZiZHQyU0g1YVRZMXNVX00tUGdralBJIiwNCiAgICAiSnpZakg0c3ZsaUgwUjNQeUVNZmVadTZKdDY5dTVxZWhabzdGN0VQWWxTRSIsDQogICAgIlBvckZicEt1VnU2eHltSmFndmtGc0ZYQWJSb2MySkdsQVVBMkJBNG83Y0kiLA0KICAgICJUR2Y0b0xiZ3dkNUpRYUh5S1ZRWlU5VWRHRTB3NXJ0RHNyWnpmVWFvbUxvIiwNCiAgICAiWFFfM2tQS3QxWHlYN0tBTmtxVlI2eVoyVmE1TnJQSXZQWWJ5TXZSS0JNTSIsDQogICAgIlh6RnJ6d3NjTTZHbjZDSkRjNnZWSzhCa01uZkc4dk9TS2ZwUElaZEFmZEUiLA0KICAgICJnYk9zSTRFZHEyeDJLdy13NXdQRXpha29iOWhWMWNSRDBBVE4zb1FMOUpNIiwNCiAgICAianN1OXlWdWx3UVFsaEZsTV8zSmx6TWFTRnpnbGhRRzBEcGZheVF3TFVLNCINCiAgXSwNCiAgImlzcyI6ICJodHRwczovL2V4YW1wbGUuY29tL2lzc3VlciIsDQogICJpYXQiOiAxNjgzMDAwMDAwLA0KICAiZXhwIjogMTg4MzAwMDAwMCwNCiAgInN1YiI6ICJ1c2VyXzQyIiwNCiAgImtleSI6IHsNCiAgICAiX3NkIjogWw0KICAgICAgImpzdTl5VnVsd1FRbGhGbE1fM0psek1hU0Z6Z2xoUUcwRHBmYXlRd0xVSzQiDQogICAgXQ0KICB9LA0KICAibmF0aW9uYWxpdGllcyI6IFsNCiAgICB7DQogICAgICAiLi4uIjogInBGbmRqa1pfVkN6bXlUYTZVamxabzNkaC1rbzhhSUtRYzlEbEd6aGFWWW8iDQogICAgfSwNCiAgICB7DQogICAgICAiLi4uIjogIjdDZjZKa1B1ZHJ5M2xjYndIZ2VaOGtoQXYxVTFPU2xlclAwVmtCSnJXWjAiDQogICAgfQ0KICBdLA0KICAiX3NkX2FsZyI6ICJzaGEtMjU2IiwNCiAgImNuZiI6IHsNCiAgICAiandrIjogew0KICAgICAgImt0eSI6ICJFQyIsDQogICAgICAiY3J2IjogIlAtMjU2IiwNCiAgICAgICJ4IjogIlRDQUVSMTladnUzT0hGNGo0VzR2ZlNWb0hJUDFJTGlsRGxzN3ZDZUdlbWMiLA0KICAgICAgInkiOiAiWnhqaVdXYlpNUUdIVldLVlE0aGJTSWlyc1ZmdWVjQ0U2dDRqVDlGMkhaUSINCiAgICB9DQogIH0NCn0.kmx687kUBiIDvKWgo2Dub-TpdCCRLZwtD7TOj4RoLsUbtFBI8sMrtH2BejXtm_P6fOAjKAVc_7LRNJFgm3PJhg~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgImdpdmVuX25hbWUiLCAiSm9obiJd~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImZhbWlseV9uYW1lIiwgIkRvZSJd~WyI2SWo3dE0tYTVpVlBHYm9TNXRtdlZBIiwgImVtYWlsIiwgImpvaG5kb2VAZXhhbXBsZS5jb20iXQ~WyJlSThaV205UW5LUHBOUGVOZW5IZGhRIiwgInBob25lX251bWJlciIsICIrMS0yMDItNTU1LTAxMDEiXQ~WyJRZ19PNjR6cUF4ZTQxMmExMDhpcm9BIiwgInBob25lX251bWJlcl92ZXJpZmllZCIsIHRydWVd~WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0~WyJQYzMzSk0yTGNoY1VfbEhnZ3ZfdWZRIiwgImJpcnRoZGF0ZSIsICIxOTQwLTAxLTAxIl0~WyJHMDJOU3JRZmpGWFE3SW8wOXN5YWpBIiwgInVwZGF0ZWRfYXQiLCAxNTcwMDAwMDAwXQ~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgIlVTIl0~WyJuUHVvUW5rUkZxM0JJZUFtN0FuWEZBIiwgIkRFIl0~" for i, testJwt := range []string{duplicateDigestSdClaimJwt, duplicateDigestArrayClaimJwt, duplicateDigestSdArrayClaimJwt, duplicateDigestNestedSdClaimJwt} { - sdJwt, err := go_sd_jwt.FromToken(testJwt, examplePublicKey) + sdJwt, err := go_sd_jwt.New(testJwt) if err == nil { t.Log("iteration: ", i) t.Error("error should be thrown") diff --git a/setters.go b/setters.go deleted file mode 100644 index 94455ac..0000000 --- a/setters.go +++ /dev/null @@ -1,21 +0,0 @@ -package go_sd_jwt - -func (d *Disclosure) setClaimName(claimName *string) { - d.claimName = claimName -} - -func (d *Disclosure) setClaimValue(claimValue string) { - d.claimValue = claimValue -} - -func (d *Disclosure) setSalt(salt string) { - d.salt = salt -} - -func (d *Disclosure) setRawValue(rawValue string) { - d.rawValue = rawValue -} - -func (d *Disclosure) setEncodedValue(encodedValue string) { - d.encodedValue = encodedValue -} diff --git a/utils.go b/utils.go deleted file mode 100644 index f8e7780..0000000 --- a/utils.go +++ /dev/null @@ -1,16 +0,0 @@ -package go_sd_jwt - -// Pointer is a helper method that returns a pointer to the given value. -func Pointer[T comparable](t T) *T { - return &t -} - -// PointerMap is a helper method that returns a pointer to the given map. -func PointerMap(m map[string]any) *map[string]any { - return &m -} - -// PointerSlice is a helper method that returns a pointer to the given slice. -func PointerSlice(s []any) *[]any { - return &s -}