com.authlete.cwt.CWTKeyProofBuilder Maven / Gradle / Ivy
Show all versions of cbor Show documentation
/*
* Copyright (C) 2024 Authlete, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.authlete.cwt;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Instant;
import java.util.Date;
import java.util.Map;
import com.authlete.cbor.CBORByteArray;
import com.authlete.cose.COSEException;
import com.authlete.cose.COSEKey;
import com.authlete.cose.COSEProtectedHeader;
import com.authlete.cose.COSEProtectedHeaderBuilder;
import com.authlete.cose.COSESign1;
import com.authlete.cose.COSESign1Builder;
import com.authlete.cose.COSESigner;
import com.authlete.cose.SigStructure;
import com.authlete.cose.SigStructureBuilder;
import com.google.gson.GsonBuilder;
import com.google.gson.ToNumberPolicy;
/**
* A utility to generate a CWT key proof, which is defined in the specification
* of "OpenID for Verifiable Credential Issuance".
*
* EXAMPLE
*
*
* // The identifier of the client application.
* String client = "my_client_id";
*
* // The identifier of the credential issuer.
* String issuer = "https://credential-issuer.example.com";
*
* // The value of 'c_nonce' issued by the authorization server
* // or the credential issuer.
* String nonce = "my_nonce";
*
* // A private key for signing. The public key corresponding to
* // this private key will be embedded in the protected header.
* COSEKey key = ...;
*
* // The issuance time. When omitted, the current time is used.
* Date iat = new Date();
*
* // Generate a CWT representing a key proof.
* {@link CWT} cwt = new CWTKeyProofBuilder()
* .{@link #setClient(String) setClient}(client)
* .{@link #setIssuer(String) setIssuer}(issuer)
* .{@link #setNonce(String) setNonce}(nonce)
* .{@link #setKey(COSEKey) setKey}(key)
* .{@link #setIssuedAt(Date) setIssuedAt}(iat)
* .{@link #build()};
*
* // The base64url representation of the key proof.
* String base64url = cwt.encodeToBase64Url();
*
*
*
* The variable "{@code base64url}" in the example above holds a value like below.
*
*
*
* 2D3ShFifowEmA3RvcGVuaWQ0dmNpLXByb29mK2N3dGhDT1NFX0tleVh7pgECAlgrMWU1QVk5RXlCMDFYblV6YTZMcEp6azAybjZZX0FtbW5TYjBGQmVOVlZyVQMmIAEhWCA9LFCsPbOXT-ZdwCrPWaCpJ4GgGGebHbLEESmsFjwXbSJYIMVfH24tRUqLFLpy3rizbi5CYqpmOkyojJ7q_hp9sEddoFhgpAFsdHJhY2sxX2xpZ2h0A3gaaHR0cHM6Ly90cmlhbC5hdXRobGV0ZS5uZXQGGmZf3KsKWCt2LTFiLW44MmtFSkdiSFJPU2VrR3NtUi14RXVhbUN4WV9UMHRYdFFOLWRZWEB0XIuOQg2CoiLJF99zotTqM80A0i5riMSgKMYzhqfAEckD2BEDIdX1X6ySkRPOAt1ftsy3HLXqg4DAPOldPZOP
*
*
*
* The following is the CBOR Diagnostic Notation representation of the example
* CWT key proof above. Refer to RFC 8949, 8. Diagnostic Notation and RFC 8610, Appendix G. Extended Diagnostic Notation for details about the
* CBOR Diagnostic Notation.
*
*
*
* 61(18(/ COSE_Sign1 / [
* / protected / <<
* {
* 1: -7,
* 3: "openid4vci-proof+cwt",
* "COSE_Key": h'a6010202582b3165354159394579423031586e557a61364c704a7a6b30326e36595f416d6d6e5362304642654e56567255032620012158203d2c50ac3db3974fe65dc02acf59a0a92781a018679b1db2c41129ac163c176d225820c55f1f6e2d454a8b14ba72deb8b36e2e4262aa663a4ca88c9eeafe1a7db0475d'
* }
* >>,
* / unprotected / {
* },
* h'a4016c747261636b315f6c6967687403781a68747470733a2f2f747269616c2e617574686c6574652e6e6574061a665fdcab0a582b762d31622d6e38326b454a476248524f53656b47736d522d784575616d4378595f5430745874514e2d6459',
* h'745c8b8e420d82a222c917df73a2d4ea33cd00d22e6b88c4a028c63386a7c011c903d8110321d5f55fac929113ce02dd5fb6ccb71cb5ea8380c03ce95d3d938f'
* ]))
*
*
*
* The value of {@code "COSE_Key"} in the protected header is a byte string,
* which wraps the COSE key. The content of the byte string is decoded as
* follows:
*
*
*
* {
* 1: 2,
* 2: h'3165354159394579423031586e557a61364c704a7a6b30326e36595f416d6d6e5362304642654e56567255',
* 3: -7,
* -1: 1,
* -2: h'3d2c50ac3db3974fe65dc02acf59a0a92781a018679b1db2c41129ac163c176d',
* -3: h'c55f1f6e2d454a8b14ba72deb8b36e2e4262aa663a4ca88c9eeafe1a7db0475d'
* }
*
*
*
* FYI: CBOR Zone (https://cbor.zone/)
* can be used to decode CBOR data.
*
*
* COMMAND LINE INVOCATION
*
*
* This class provides the {@link #main(String[])} method for invocation
* from the command line. Refer to the JavaDoc of the method for details.
* The generate-cwt-key-proof
script is a wrapper for the command line
* invocation.
*
*
* @since 1.15
*
* @see OpenID for Verifiable Credential Issuance
*/
public class CWTKeyProofBuilder
{
private static final String CONTENT_TYPE = "openid4vci-proof+cwt";
private static final String LABEL_COSE_KEY = "COSE_Key";
private String client;
private String issuer;
private Date issuedAt;
private String nonce;
private COSEKey key;
/**
* The default constructor.
*/
public CWTKeyProofBuilder()
{
}
private CWTKeyProofBuilder(Options options)
{
this.client = options.client;
this.issuer = options.issuer;
this.nonce = options.nonce;
this.key = options.key;
this.issuedAt = options.issuedAt;
}
/**
* Get the identifier of the client application. This value is used as the
* value of the Claim Key 1 ({@code iss}).
*
*
* In most cases, this parameter is mandatory. Omission of this parameter is
* allowed only when the access token has been issued by the pre-authorized
* code flow with anonymous access. Refer to "OpenID for Verifiable Credential
* Issuance" for details.
*
*
* @return
* The identifier of the client application.
*
* @see IANA: CBOR Web Token (CWT) Claims
*/
public String getClient()
{
return client;
}
/**
* Set the identifier of the client application. This value is used as the
* value of the Claim Key 1 ({@code iss}).
*
*
* In most cases, this parameter is mandatory. Omission of this parameter is
* allowed only when the access token has been issued by the pre-authorized
* code flow with anonymous access. Refer to "OpenID for Verifiable Credential
* Issuance" for details.
*
*
* @param client
* The identifier of the client application.
*
* @return
* {@code this} object.
*
* @see IANA: CBOR Web Token (CWT) Claims
*/
public CWTKeyProofBuilder setClient(String client)
{
this.client = client;
return this;
}
/**
* Get the identifier of the credential issuer. This value is used as the
* value of the Claim Key 3 ({@code aud}).
*
*
* This parameter is mandatory.
*
*
* @return
* The identifier of the credential issuer.
*
* @see IANA: CBOR Web Token (CWT) Claims
*/
public String getIssuer()
{
return issuer;
}
/**
* Set the identifier of the credential issuer. This value is used as the
* value of the Claim Key 3 ({@code aud}).
*
*
* This parameter is mandatory.
*
*
* @param issuer
* The identifier of the credential issuer.
*
* @return
* {@code this} object.
*
* @see IANA: CBOR Web Token (CWT) Claims
*/
public CWTKeyProofBuilder setIssuer(String issuer)
{
this.issuer = issuer;
return this;
}
/**
* Get the issuance time. This value is used as the value of the Claim Key 6
* ({@code iat}).
*
*
* If this parameter has not been specified, the {@link #build()} method uses
* the current time.
*
*
* @return
* The issuance time.
*
* @see IANA: CBOR Web Token (CWT) Claims
*/
public Date getIssuedAt()
{
return issuedAt;
}
/**
* Set the issuance time. This value is used as the value of the Claim Key 6
* ({@code iat}).
*
*
* If this parameter has not been specified, the {@link #build()} method uses
* the current time.
*
*
* @param issuedAt
* The issuance time.
*
* @return
* {@code this} object.
*
* @see IANA: CBOR Web Token (CWT) Claims
*/
public CWTKeyProofBuilder setIssuedAt(Date issuedAt)
{
this.issuedAt = issuedAt;
return this;
}
/**
* Get the nonce value, which is the value of "{@code c_nonce}" issued by
* the server (the authorization server or the credential issuer). This value
* is used as the value of the Claim Key 10 ({@code Nonce}).
*
*
* If a {@code c_nonce} has been issued by the server (the authorization
* server or the credential issuer), this parameter is mandatory.
*
*
* @return
* The nonce value.
*
* @see IANA: CBOR Web Token (CWT) Claims
*/
public String getNonce()
{
return nonce;
}
/**
* Set the nonce value, which is the value of "{@code c_nonce}" issued by
* the server (the authorization server or the credential issuer). This value
* is used as the value of the Claim Key 10 ({@code Nonce}).
*
*
* If a {@code c_nonce} has been issued by the server (the authorization
* server or the credential issuer), this parameter is mandatory.
*
*
* @param nonce
* The nonce value.
*
* @return
* {@code this} object.
*
* @see IANA: CBOR Web Token (CWT) Claims
*/
public CWTKeyProofBuilder setNonce(String nonce)
{
this.nonce = nonce;
return this;
}
/**
* Get the private key.
*
*
* The private key is used for signing the CWT key proof. In addition, the
* public key corresponding to the private key is embedded in the protected
* header of the CWT key proof as the value of "{@code COSE_Key}".
*
*
*
* In the current implementation, the key must be an instance of
* {@link com.authlete.cose.COSEEC2Key COSEEC2Key}, because supported key
* algorithms are {@code ES256}, {@code ES384} and {@code ES512} only.
*
*
* @return
* The private key.
*/
public COSEKey getKey()
{
return key;
}
/**
* Set the private key.
*
*
* The private key is used for signing the CWT key proof. In addition, the
* public key corresponding to the private key is embedded in the protected
* header of the CWT key proof as the value of "{@code COSE_Key}".
*
*
*
* In the current implementation, the key must be an instance of
* {@link com.authlete.cose.COSEEC2Key COSEEC2Key}, because supported key
* algorithms are {@code ES256}, {@code ES384} and {@code ES512} only.
*
*
* @param key
* The private key.
*
* @return
* {@code this} object.
*/
public CWTKeyProofBuilder setKey(COSEKey key)
{
this.key = key;
return this;
}
/**
* Generate a CWT key proof.
*
*
* At least the issuer and the private key must be set before this method
* is called.
*
*
* @return
* A {@link CWT} instance that represents a CWT key proof.
*
* @throws IllegalStateException
* A mandatory parameter is not set, or the specified key does not
* satisfy required conditions.
*
* @throws COSEException
* Failed to generate a CWT key proof.
*/
public CWT build() throws IllegalStateException, COSEException
{
// Check the state of this CWTKeyProofBuilder instance to ensure
// that necessary pieces of information have been set properly
// for executing this 'build()' method.
checkState();
// The private key. This is used for signing. In addition, the
// public key corresponding to this private key is embedded in
// the protected header.
COSEKey key = getKey();
// Protected header
COSEProtectedHeader protectedHeader = buildProtectedHeader(key);
// Payload
CBORByteArray payload = buildPayload();
// Signature
byte[] signature = buildSignature(protectedHeader, payload, key);
// COSE_Sign1
COSESign1 sign1 = buildSign1(protectedHeader, payload, signature);
// CWT
return new CWT(sign1);
}
private void checkState()
{
checkIssuer();
checkKey();
}
private void checkIssuer()
{
// If 'issuer' is set.
if (getIssuer() != null)
{
// OK.
return;
}
// 'issuer' is necessary as it is used as the value of 'aud'.
throw new IllegalStateException("'issuer' is not set.");
}
private void checkKey()
{
COSEKey key = getKey();
// If 'key' is not set.
if (key == null)
{
throw new IllegalStateException("'key' is not set.");
}
// If the key is not a private key.
if (!key.isPrivate())
{
throw new IllegalStateException("The key is not a private key.");
}
// The algorithm of the key.
Object alg = key.getAlg();
// If the representation of the algorithm is not a number.
if (!(alg instanceof Number))
{
throw new IllegalStateException("The representation of the algorithm of the key is not a number.");
}
}
private static COSEProtectedHeader buildProtectedHeader(COSEKey key) throws COSEException
{
// OpenID for Verifiable Credential Issuance
// 7.2.1.3. cwt Proof Type
//
// Label 1 (alg):
// REQUIRED. A digital signature algorithm identifier such as per
// IANA "COSE Algorithms" registry [IANA.COSE.ALGS]. It MUST NOT
// be an identifier for a symmetric algorithm (MAC).
//
// Label 3 (content type):
// REQUIRED. MUST be openid4vci-proof+cwt, which explicitly types
// the key proof CWT.
//
// (string-valued) Label COSE_Key:
// OPTIONAL (byte string). COSE key material the new Credential
// shall be bound to. It MUST NOT be present if x5chain is present.
//
// Label 33 (x5chain):
// OPTIONAL (byte string). As defined in [RFC9360], it contains an
// ordered array of X.509 certificates corresponding to the key used
// to sign the CWT. It MUST NOT be present if COSE_Key is present.
//
// Convert the private key to the corresponding public key.
COSEKey pubKey = key.toPublic();
// Wrap the public key into a byte string.
CBORByteArray pubKeyEmbedded = new CBORByteArray(pubKey.encode(), pubKey);
return new COSEProtectedHeaderBuilder()
.alg(key.getAlg())
.contentType(CONTENT_TYPE)
.put(LABEL_COSE_KEY, pubKeyEmbedded)
.build();
}
private CBORByteArray buildPayload()
{
CWTClaimsSet claims = buildClaims();
// The CBOR representation of the CWT Claims Set.
byte[] encodedClaims = claims.encode();
// Embed the encoded claims into a byte string.
return new CBORByteArray(encodedClaims);
}
private CWTClaimsSet buildClaims()
{
// OpenID for Verifiable Credential Issuance
// 7.2.1.3. cwt Proof Type
//
// Claim Key 1 (iss):
// OPTIONAL (text string). The value of this claim MUST be the
// client_id of the Client making the Credential request. This
// claim MUST be omitted if the access token authorizing the
// issuance call was obtained from a Pre-Authorized Code Flow
// through anonymous access to the token endpoint.
//
// Claim Key 3 (aud):
// REQUIRED (text string). The value of this claim MUST be the
// Credential Issuer Identifier.
//
// Claim Key 6 (iat):
// REQUIRED (integer or floating-point number). The value of
// this claim MUST be the time at which the key proof was issued.
//
// Claim Key 10 (Nonce):
// OPTIONAL (byte string). The value of this claim MUST be a
// server-provided c_nonce converted from string to bytes. It
// MUST be present when the Wallet received a server-provided
// c_nonce.
//
CWTClaimsSetBuilder builder = new CWTClaimsSetBuilder();
// Claim Key 1 (iss)
if (getClient() != null)
{
builder.iss(getClient());
}
// Claim Key 3 (aud)
builder.aud(getIssuer());
// Claim Key 6 (iat)
builder.iat(determineIssuedAt());
// Claim Key 10 (Nonce)
if (getNonce() != null)
{
builder.nonce(getNonce());
}
return builder.build();
}
private Date determineIssuedAt()
{
Date iat = getIssuedAt();
// If the issuance time has not been set explicitly.
if (iat == null)
{
// Use the current time.
iat = new Date();
}
return iat;
}
private static byte[] buildSignature(
COSEProtectedHeader protectedHeader, CBORByteArray payload,
COSEKey signingKey) throws COSEException
{
// Sig_structure
SigStructure structure = buildSigStructure(protectedHeader, payload);
// Signer
COSESigner signer = createSigner(signingKey);
// The algorithm identifier as an integer.
int alg = ((Number)signingKey.getAlg()).intValue();
// The key ID (optional).
byte[] kid = signingKey.getKid();
// Sign the Sig_structure (= generate a signature).
return signer.sign(structure, alg, kid);
}
private static SigStructure buildSigStructure(
COSEProtectedHeader protectedHeader, CBORByteArray payload)
{
// Create a Sig_structure for COSE_Sign1.
return new SigStructureBuilder()
.signature1()
.bodyAttributes(protectedHeader)
.payload(payload)
.build();
}
private static COSESigner createSigner(COSEKey signingKey) throws COSEException
{
return new COSESigner(signingKey.createPrivateKey());
}
private static COSESign1 buildSign1(
COSEProtectedHeader protectedHeader, CBORByteArray payload, byte[] signature)
{
// Create a COSE_Sign1.
return new COSESign1Builder()
.protectedHeader(protectedHeader)
.payload(payload)
.signature(signature)
.build();
}
/**
* The entry point for invocation from the command line.
* The following command line options are recognized.
*
*
*
*
* option
* description
*
*
* --issuer ISSUER
*
*
* REQUIRED. This option specifies the identifier of the credential issuer.
*
*
*
*
* --key FILE
*
*
* REQUIRED. This option specifies the file containing a private key in the
* JWK format (RFC 7517:
* JSON Web Key (JWK)).
*
*
*
*
* --client CLIENT
*
*
* OPTIONAL. This option specifies the identifier of the client application.
*
*
*
*
* --nonce NONCE
*
*
* OPTIONAL. This option specifies the value of "{@code c_nonce}" that has
* been issued by the authorization server or the credential issuer.
*
*
*
*
* --issued-at TIME
*
*
* OPTIONAL. This option specifies the issuance time by using one of the
* following formats:
*
*
* - integer representing seconds since the Unix epoch
*
- string representing a datetime in UTC in the ISO 8601 format
*
*
* When this option is omitted, the current time is used as the issuance time.
*
*
*
*
* --help
*
*
* This option shows the help text.
*
*
*
*
*
*
* @param args
* The command line arguments.
*/
public static void main(String[] args)
{
try
{
// Process the command line arguments.
Options options = new Options().process(args);
// Build a CWT key proof.
CWT cwt = new CWTKeyProofBuilder(options).build();
// The base64url representation of the CWT key proof.
String rep = cwt.encodeToBase64Url();
// Print the representation.
System.out.println(rep);
}
catch (Exception cause)
{
System.err.format("ERROR: %s%n", cause.getMessage());
if (!(cause instanceof IllegalArgumentException))
{
cause.printStackTrace(System.err);
}
System.exit(1);
}
}
private static final String HELP =
"USAGE:%n%n" +
" java %s%n" +
" --issuer ISSUER # specifies the identifier of the credential issuer.%n" +
" --key FILE # specifies the file containing a private key in the JWK format.%n" +
" [--client CLIENT] # specifies the identifier of the client application.%n" +
" [--nonce NONCE] # specifies the 'c_nonce' value issued by the server.%n" +
" [--issued-at TIME] # specifies the issuance time.%n" +
" [--help] # shows this help text.%n%n" +
"NOTE:%n%n" +
" Supported key algorithms are ES256, ES384 and ES512 only.%n%n" +
" The issuance time can be specified by one of the following formats:%n" +
" (a) integer representing seconds since the Unix epoch%n" +
" (b) string representing a datetime in UTC in the ISO 8601 format%n%n"
;
private static class Options
{
public String client;
public String issuer;
public String nonce;
public COSEKey key;
public Date issuedAt;
public Options process(String[] args) throws IOException, COSEException
{
parse(args);
validate();
return this;
}
private void parse(String[] args) throws IOException, COSEException
{
for (int i = 0; i < args.length; i++)
{
String arg = args[i];
switch (arg)
{
case "--client":
client = next(arg, args, ++i);
break;
case "--issuer":
issuer = next(arg, args, ++i);
break;
case "--nonce":
nonce = next(arg, args, ++i);
break;
case "--key":
String file = next(arg, args, ++i);
key = readKey(file);
break;
case "--issued-at":
String issuedAtStr = next(arg, args, ++i);
issuedAt = readIssuedAt(issuedAtStr);
break;
case "--help":
help(0);
break;
default:
System.err.format("ERROR: Unexpected argument: %s%n%n", arg);
help(1);
break;
}
}
}
private String next(String arg, String[] args, int index)
{
if (args.length <= index)
{
throw new IllegalArgumentException(String.format(
"The option '%s' requires a following argument.", arg));
}
return args[index];
}
@SuppressWarnings("unchecked")
private COSEKey readKey(String file) throws IOException, COSEException
{
// Path
Path path = Paths.get(file);
// The content in bytes.
byte[] bytes = Files.readAllBytes(path);
// Convert the bytes into a string.
String string = new String(bytes, StandardCharsets.UTF_8);
// Interpret the string as a JSON object, which should represent a JWK.
Map map = new GsonBuilder()
.setObjectToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE)
.create()
.fromJson(string, Map.class);
// Create a COSEKey instance from the map representing a JWK.
return COSEKey.fromJwk(map);
}
private Date readIssuedAt(String issuedAtStr)
{
try
{
// Try to parse the input string as an integer representing
// seconds since the Unix epoch.
long seconds = Long.parseLong(issuedAtStr);
// Convert the seconds into milliseconds, and create a Date instance.
return new Date(seconds * 1000L);
}
catch (Exception cause)
{
// Failed to parse the input string as an integer.
}
try
{
// Try to parse the input string as a string representing
// a datetime in UTC in the ISO 8601 format.
Instant instant = Instant.parse(issuedAtStr);
// Convert the Instant instance to a Date instance.
return Date.from(instant);
}
catch (Exception cause)
{
// The value specified by the '--issued-at' option is malformed.
throw new IllegalArgumentException(String.format(
"The value specified by the '--issued-at' option is malformed: %s", issuedAtStr));
}
}
private void help(int exitStatus)
{
// Show the help text.
System.out.format(HELP, CWTKeyProofBuilder.class.getName());
System.exit(exitStatus);
}
private void validate()
{
if (issuer == null)
{
throw new IllegalArgumentException(
"The '--issuer ISSUER' option is mandatory.");
}
if (key == null)
{
throw new IllegalArgumentException(
"The '--key FILE' option is mandatory.");
}
}
}
}