-
Notifications
You must be signed in to change notification settings - Fork 10
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implements SD-JWT #103
base: main
Are you sure you want to change the base?
Implements SD-JWT #103
Conversation
Codecov Report
Additional details and impacted files@@ Coverage Diff @@
## main #103 +/- ##
==========================================
+ Coverage 78.96% 79.91% +0.95%
==========================================
Files 33 41 +8
Lines 1930 2788 +858
Branches 265 477 +212
==========================================
+ Hits 1524 2228 +704
- Misses 274 378 +104
- Partials 132 182 +50
|
} | ||
|
||
// Check that the SD-JWT is valid using nbf, iat, and exp claims, if provided in the SD-JWT, and not selectively disclosed. | ||
val claimsVerifier = DefaultJWTClaimsVerifier<SecurityContext>(null, null) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are these nulls in the constructor expected?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. Those provide fields that are required. passing null validated the nbf, iat, and exp claims only when they're present.
|
||
// Ensure that a signing algorithm was used that was deemed secure for the application. Refer to [RFC8725], Sections 3.1 | ||
// and 3.2 for details. | ||
require(verificationOptions.supportedAlgorithms.contains(keyBindingJwt.header.algorithm)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
did similar supportedAlgorithms above, maybe pull out into a fuction
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't quite follow the suggestion, mind elaborating?
"supported algorithms (${verificationOptions.supportedAlgorithms})" | ||
} | ||
// The none algorithm MUST NOT be accepted. | ||
require(keyBindingJwt.header.algorithm != JWSAlgorithm.NONE) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
verificationOptions.supportedAlgorithms
are meant to be user specified, BUT the spec explicitly says that NONE cannot be part of it. Let me know if that makes sense. If not, mind making a concrete suggestion to improve?
|
||
// Ensure that the disclosure is object or array disclosure | ||
when (disclosureElems.size) { | ||
3 -> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fancy fancy
) | ||
} | ||
|
||
else -> throw IllegalArgumentException("Disclosure \"$encodedDisclosure\" must have exactly 3 elements") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this correct? Seems like there is a case to handle 2 elements
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed, thx!
|
||
repeat(totalDigests(arrayDisclosures.size) - arrayDisclosures.size) { | ||
val randBytes = ByteArray(32) | ||
SecureRandom().nextBytes(randBytes) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We so need external audits on this stuff :hahaha:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Curious what parts were red flags?
|
||
|
||
/** Interface for generating salt values as described in https://www.ietf.org/archive/id/draft-ietf-oauth-selective-disclosure-jwt-05.html#disclosures_for_object_properties */ | ||
public interface ISaltGenerator { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there not a library for this? Is this a special case sd-jwt salt?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There isn't a library that does exactly what's needed. This module provides an implementation below, which is used by default. It can be overriden, which was super useful for tests.
|
||
} | ||
|
||
private fun getNextPowerOfTwo(n: Int): Int { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
wow some leetcode stuff
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
More like CHAD stuff hahaha
"zip_code": "12345" | ||
} | ||
}""".trimIndent() | ||
val claimsToBlind = mapOf( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why can't this be dynamic? Is it to give the option to the user?
If they provide null as claimsToBlind can it dynamically do default blinding to all?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The issuer of an SdJwt must, at some point, select which claims they'll allow the holder to selectively disclose. For those, they must also say how they'll be disclosable, since there are various mechanisms.
What do you think should be the way in which they are all blinded by default?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wow mind blown. Great stuff! I need to reread the sd-jwt spec and look over one more time.
Will this only be used for credentials? Does it make sense to put it in a sub package of credentials or something?
There will be more changes in the credentials package to make it user friendly to get SD JWT creds along with the normal cred they made?
VerifiableCredential.create(sdJwt=ture) // something like this?
No, it can be used for other types credentials, that aren't w3c vcs.
Yes, but that comes later. Once we have support for VC2.0, then we'll need to allow securing it using sd-jwt. That will likely be the piece that does belong inside the
Something like that sounds reasonable. |
* Verifies this SD-JWT according to [verificationOptions]. | ||
*/ | ||
@Throws(Exception::class) | ||
public fun verify( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe add some more detailed explanations on these comments, similar to our other packages, and add example code if it is short and sweet
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added more documentation, and an example.
public fun unblind( | ||
serializedSdJwt: String | ||
): JWTClaimsSet { | ||
// 2. Separate the Presentation into the SD-JWT, the Disclosures (if any), and the Holder Binding JWT (if provided). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// 2. Separate the Presentation into the SD-JWT, the Disclosures (if any), and the Holder Binding JWT (if provided). | |
// Separate the Presentation into the SD-JWT, the Disclosures (if any), and the Holder Binding JWT (if provided). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
val disclosuresByDigest = sdJwt.disclosures.associateBy { it.digest(hashAlg) } | ||
|
||
// Process the Disclosures and _sd keys in the SD-JWT as follows: | ||
// |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
} | ||
|
||
@Test | ||
fun testExample1() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fix the test names in this file
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed, and added where they came from
val keyManager = InMemoryKeyManager() | ||
DidKey.create(keyManager) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
are these lines needed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
They aren't! Removed.
settings.gradle.kts
Outdated
@@ -1,3 +1,4 @@ | |||
rootProject.name = "web5" | |||
include("common", "crypto", "dids", "credentials") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
include("common", "crypto", "dids", "credentials") | |
include("common", "crypto", "dids", "credentials", "sdjwt") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, separated everything into single lines and sorted alphabetically.
Why is this another package? In the future should we abstract like a lot of this away and have something like:
Am I thinking about this the wrong way? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Happy to get this in since it is isolated in another package, but I want to see how we fit this into the bigger picture of our VerifiableCredential packages.
Would love to the see the api interface easier to understand and/or integrated in our existing VC package if possible
return when (jwk) { | ||
is ECKey -> jwk.toPublicKey() | ||
is OctetKeyPair -> jwk.toPublicKey() | ||
is OctetSequenceKey -> jwk.toSecretKey() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why is this secret but the others are public?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ooof, good catch. Removed this line. Should only be using asymmetric keys.
) | ||
} | ||
|
||
2 -> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: put 2 before 3
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lgtm - would appreciate a review from @mistermoe or @phoebe-lew before merging
* Note: While [issuerJwt] and [keyBindingJwt] are of type [SignedJWT], they may or may not be signed. | ||
*/ | ||
public class SdJwt( | ||
public val issuerJwt: SignedJWT, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So in order to use SD-JWT, if for example if I have VC in JWT format created by the web5/credentials
package, do I first need to convert it into the nimbusds SignedJWT
object?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes you do.
Assuming you have an object of type String
that was returned from the VerifiableCredential.sign
function, you can do something like this:
val vcJwtString = ""
val issuerJwt = SignedJWT.parse(vcJwtString)
The motivation behind this design for a the SdJwt
object was to:
- Reduce cognitive overhead by only allowing specific types instead of generic primitives (i.e. SignedJWT vs String).
- Allow for others to use the sd-jwt library by itself by decoupling it from other existing packages in web5. Later on, changes can be made to web5/credentials and web5/dids to make it easy to create an sd-jwt that contains a VC.
/** | ||
* Represents a disclosure for an Object Property as defined in https://www.ietf.org/archive/id/draft-ietf-oauth-selective-disclosure-jwt-05.html#name-disclosures-for-object-prop. | ||
*/ | ||
public class ObjectDisclosure( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe move out disclosure classes into separate file?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
return Pair(blinded, allDisclosures) | ||
} | ||
|
||
fun toBlindedClaimsAndDisclosures( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@andresuribe87 can you add comments or doc for this function? For code readability rather than external documentation
} | ||
|
||
@Test | ||
fun `whole flow from issuer to holder to verifier`() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
super helpful to understand how the whole thing works!
.build() | ||
val sdJwt = builder.build() | ||
|
||
sdJwt.signAsIssuer(issuerSigner) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can I not sign this with my did?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No. As mentioned in #103 (comment), the motivation was to make this a lower level lib. What you're suggesting is an excellent idea, and I would encourage that to be done in a separate PR that changes the public APIs that exist in the web5/credentials
module.
Overview
Implements a module that supports SD-JWT according to https://www.ietf.org/archive/id/draft-ietf-oauth-selective-disclosure-jwt-05.htm
This PR will be a first step towards fully closing #56
Description
SD-JWT is becoming the de-factor standard. This PR implements the basic functionality in order to support SD-JWT from the perspective of the holder, the issuer, and the verifier. Simple documentation has been added in this iteration. It's published as a separate module, and it's dependencies are kept lean on purpose, since this module is meant to be used independently from DIDs and VCs.
Most of the design takes inspiration from the Nimbus library, where a JWT needs to be created first, and then signed.
Feedback would be much appreciated.
How Has This Been Tested?
Unit tests everywhere.