Skip to content

Commit 70f242c

Browse files
committed
Flesh out attestation certs and largeBlobStore
1 parent 19962c9 commit 70f242c

File tree

4 files changed

+152
-59
lines changed

4 files changed

+152
-59
lines changed

install_attestation_cert.py

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,16 @@
22

33
import sys
44
import argparse
5+
import base64
56

67
from fido2.ctap2 import Ctap2
78
from fido2.ctap2.base import args as ctap_args
89
from fido2.pcsc import CtapPcscDevice
910

11+
import secrets
12+
13+
from cryptography.hazmat.primitives.asymmetric import ec
14+
1015
from python_tests.ctap.ctap_test import BasicAttestationTestCase
1116

1217
if __name__ == '__main__':
@@ -17,17 +22,47 @@
1722
parser.add_argument('--aaguid',
1823
default=None,
1924
help='AAGUID to use, expressed as 16 hex bytes (32-character-long string)')
25+
parser.add_argument('--ca-cert-bytes',
26+
default=None,
27+
help='CA certificate, expressed as base64-encoded DER')
28+
parser.add_argument('--ca-private-key',
29+
default=None,
30+
help='CA private key, expressed as a hex string')
31+
parser.add_argument('--org',
32+
default='ACME',
33+
help='Organization name to use for certificates')
34+
parser.add_argument('--country',
35+
default='US',
36+
help='ISO country code to use for certificates')
2037
args = parser.parse_args()
2138

39+
if (args.ca_private_key is None) != (args.ca_cert_bytes is None):
40+
raise IllegalArgumentException("Either both or neither of CA certificate and private key must be set")
41+
2242
aaguid = None
2343
if args.aaguid is not None:
2444
if len(args.aaguid) != 32:
2545
sys.stderr.write("Invalid AAGUID length!\n")
2646
sys.exit(1)
2747
aaguid = bytes.fromhex(args.aaguid)
48+
else:
49+
aaguid = secrets.token_bytes(16)
2850

2951
tc = BasicAttestationTestCase()
30-
at_bytes = tc.gen_attestation_cert(name=args.name, aaguid=aaguid)
52+
if args.ca_private_key is None:
53+
ca_privkey_and_cert = tc.get_ca_cert(org=args.org)
54+
else:
55+
ca_privkey_and_cert = bytes.fromhex(args.ca_private_key), base64.b64decode(args.ca_cert_bytes)
56+
private_key = ec.generate_private_key(ec.SECP256R1())
57+
58+
cert_bytes = tc.get_x509_certs(private_key, name=args.name, ca_privkey_and_cert=ca_privkey_and_cert,
59+
org=args.org, country=args.country)
60+
print(f"Using AAGUID: {aaguid.hex()}")
61+
print(f"Using CA certificate: {base64.b64encode(cert_bytes[-1])}")
62+
63+
at_bytes = tc.assemble_cbor_from_attestation_certs(private_key=private_key,
64+
cert_bytes=cert_bytes,
65+
aaguid=aaguid)
3166

3267
devices = list(CtapPcscDevice.list_devices())
3368
if len(devices) > 1:
@@ -42,4 +77,4 @@
4277
0x46,
4378
ctap_args(at_bytes)
4479
)
45-
print(f"Got response: {res}")
80+
print(f"Got response: {res} (empty is good)")

mds.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,9 @@
3131
"cryptoStrength": 128,
3232
"attachmentHint": ["nfc"],
3333
"tcDisplay": [],
34-
"attestationRootCertificates": [],
34+
"attestationRootCertificates": [
35+
36+
],
3537
"authenticatorGetInfo": {
3638
"versions": [ "FIDO_2_0", "FIDO_2_1", "FIDO_2_1_PRE", "U2F_V2" ],
3739
"extensions": [ "uvm", "credBlob", "credProtect", "hmac-secret", "largeBlobKey" ],

python_tests/ctap/ctap_test.py

Lines changed: 72 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -476,35 +476,14 @@ def install_attestation_cert(self, **kwargs):
476476
def _short_to_bytes(self, b: int) -> list[int]:
477477
return [(b & 0xFF00) >> 8, b & 0x00FF]
478478

479-
def get_x509_certs(self, private_key: EllipticCurvePrivateKey, name: Optional[str] = None,
480-
country: Optional[str] = "US", org: Optional[str] = "ACME") -> list[bytes]:
481-
self.public_key = private_key.public_key()
482-
483-
if name is None:
484-
name = secrets.token_hex(4)
485-
479+
def gen_authenticator_cert_from_ca(self, name: str,
480+
ca_name: x509.Name,
481+
ca_privkey: EllipticCurvePrivateKey,
482+
country: Optional[str] = "US",
483+
org: Optional[str] = "ACME",
484+
) -> bytes:
486485
now = datetime.now()
487486

488-
ca_privkey = ec.generate_private_key(ec.SECP256R1())
489-
ca_pubkey = ca_privkey.public_key()
490-
self.ca_public_key = ca_pubkey
491-
492-
ca_name = x509.Name([
493-
x509.NameAttribute(NameOID.ORGANIZATION_NAME, org),
494-
x509.NameAttribute(NameOID.COMMON_NAME, "AuthCA"),
495-
])
496-
ca_cert_bytes = (
497-
x509.CertificateBuilder()
498-
.subject_name(ca_name)
499-
.issuer_name(ca_name)
500-
.serial_number(x509.random_serial_number())
501-
.public_key(ca_pubkey)
502-
.not_valid_before(now - timedelta(days=1))
503-
.not_valid_after(now + timedelta(days=3650))
504-
.sign(private_key=ca_privkey, algorithm=hashes.SHA256())
505-
.public_bytes(Encoding.DER)
506-
)
507-
508487
this_cert_name = x509.Name([
509488
x509.NameAttribute(NameOID.COUNTRY_NAME, country),
510489
x509.NameAttribute(NameOID.ORGANIZATION_NAME, org),
@@ -530,19 +509,60 @@ def get_x509_certs(self, private_key: EllipticCurvePrivateKey, name: Optional[st
530509
.sign(private_key=ca_privkey, algorithm=hashes.SHA256())
531510
.public_bytes(Encoding.DER)
532511
)
533-
return [authenticator_cert_bytes, ca_cert_bytes]
534512

535-
def gen_attestation_cert(self, cert_bytes: Optional[list[bytes]] = None, name: Optional[str] = None,
536-
aaguid: Optional[bytes] = None) -> bytes:
537-
if aaguid is None:
538-
aaguid = secrets.token_bytes(16)
513+
return authenticator_cert_bytes
539514

540-
self.aaguid = aaguid
541-
private_key = ec.generate_private_key(ec.SECP256R1())
515+
def get_ca_cert(self, org: str) -> tuple[bytes, bytes]:
516+
now = datetime.now()
542517

543-
if cert_bytes is None:
544-
cert_bytes = self.get_x509_certs(private_key, name)
518+
ca_privkey = ec.generate_private_key(ec.SECP256R1())
519+
ca_pubkey = ca_privkey.public_key()
520+
self.ca_public_key = ca_pubkey
521+
522+
ca_name = x509.Name([
523+
x509.NameAttribute(NameOID.ORGANIZATION_NAME, org),
524+
x509.NameAttribute(NameOID.COMMON_NAME, "AuthCA"),
525+
])
526+
return ca_privkey, (
527+
x509.CertificateBuilder()
528+
.subject_name(ca_name)
529+
.issuer_name(ca_name)
530+
.serial_number(x509.random_serial_number())
531+
.public_key(ca_pubkey)
532+
.not_valid_before(now - timedelta(days=1))
533+
.not_valid_after(now + timedelta(days=3650))
534+
.sign(private_key=ca_privkey, algorithm=hashes.SHA256())
535+
.public_bytes(Encoding.DER)
536+
)
537+
538+
def get_x509_certs(self, private_key: EllipticCurvePrivateKey, name: Optional[str] = None,
539+
country: Optional[str] = "US", org: Optional[str] = "ACME",
540+
ca_privkey_and_cert: Optional[tuple[bytes, bytes]] = None) -> list[bytes]:
541+
self.public_key = private_key.public_key()
542+
543+
if name is None:
544+
name = secrets.token_hex(4)
545545

546+
if ca_privkey_and_cert is None:
547+
ca_privkey_and_cert = self.get_ca_cert(org)
548+
549+
ca_privkey, ca_cert_bytes = ca_privkey_and_cert
550+
551+
loaded_cacert = x509.load_der_x509_certificate(ca_cert_bytes)
552+
ca_name = loaded_cacert.subject
553+
554+
authenticator_cert_bytes = self.gen_authenticator_cert_from_ca(
555+
name=name,
556+
ca_name=ca_name,
557+
ca_privkey=ca_privkey,
558+
country=country,
559+
org=org
560+
)
561+
562+
return [authenticator_cert_bytes, ca_cert_bytes]
563+
564+
def assemble_cbor_from_attestation_certs(self, private_key: bytes, cert_bytes: list[bytes],
565+
aaguid: bytes) -> bytes:
546566
num_certs = len(cert_bytes)
547567
self.cert = cert_bytes[0]
548568
cert_cbor_bytes = [0x80 + num_certs]
@@ -564,9 +584,24 @@ def gen_attestation_cert(self, cert_bytes: Optional[list[bytes]] = None, name: O
564584
private_bytes = s.to_bytes(length=32)
565585
self.assertEqual(32, len(private_bytes))
566586
cbor_len_bytes = bytes(self._short_to_bytes(len(cert_cbor)))
567-
res = self.aaguid + private_bytes + cbor_len_bytes + cert_cbor
587+
res = aaguid + private_bytes + cbor_len_bytes + cert_cbor
568588
return res
569589

590+
def gen_attestation_cert(self, cert_bytes: Optional[list[bytes]] = None, name: Optional[str] = None,
591+
aaguid: Optional[bytes] = None) -> bytes:
592+
if aaguid is None:
593+
aaguid = secrets.token_bytes(16)
594+
595+
self.aaguid = aaguid
596+
private_key = ec.generate_private_key(ec.SECP256R1())
597+
598+
if cert_bytes is None:
599+
cert_bytes = self.get_x509_certs(private_key, name)
600+
601+
return self.assemble_cbor_from_attestation_certs(private_key=private_key,
602+
cert_bytes=cert_bytes,
603+
aaguid=aaguid)
604+
570605

571606
class FixedPinUserInteraction(UserInteraction):
572607
pin: str

src/main/java/us/q3q/fido2/FIDO2Applet.java

Lines changed: 40 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -348,7 +348,8 @@ public final class FIDO2Applet extends Applet implements ExtendedLength {
348348
/**
349349
* Storage for the largeBlobs extension
350350
*/
351-
private final byte[][] largeBlobStores;
351+
private final byte[] largeBlobStoreA;
352+
private final byte[] largeBlobStoreB;
352353
/**
353354
* Which large blob store is in use
354355
*/
@@ -3648,6 +3649,20 @@ public void process(APDU apdu) throws ISOException {
36483649
transientStorage.resetChainIncomingReadOffset();
36493650
}
36503651

3652+
private byte[] getCurrentLargeBlobStore() {
3653+
if (largeBlobStoreIndex == 0) {
3654+
return largeBlobStoreA;
3655+
}
3656+
return largeBlobStoreB;
3657+
}
3658+
3659+
private byte[] getInactiveLargeBlobStore() {
3660+
if (largeBlobStoreIndex == 1) {
3661+
return largeBlobStoreA;
3662+
}
3663+
return largeBlobStoreB;
3664+
}
3665+
36513666
/**
36523667
* Processes CTAP2 largeBlob extension commands
36533668
*
@@ -3773,7 +3788,9 @@ private void handleLargeBlobs(APDU apdu, byte[] reqBuffer, short lc) {
37733788
sendErrorByte(apdu, FIDOConstants.CTAP1_ERR_INVALID_PARAMETER);
37743789
}
37753790

3776-
if (offset < 0 || offset > (short) largeBlobStores[largeBlobStoreIndex].length) {
3791+
byte[] currentLargeBlobStore = getCurrentLargeBlobStore();
3792+
3793+
if (offset < 0 || offset > (short) currentLargeBlobStore.length) {
37773794
// Offset is mandatory and must be reasonable
37783795
sendErrorByte(apdu, FIDOConstants.CTAP1_ERR_INVALID_PARAMETER);
37793796
}
@@ -3804,7 +3821,7 @@ private void handleLargeBlobs(APDU apdu, byte[] reqBuffer, short lc) {
38043821
if (setTotalLength < 17) {
38053822
sendErrorByte(apdu, FIDOConstants.CTAP1_ERR_INVALID_PARAMETER);
38063823
}
3807-
if (setTotalLength > (short) largeBlobStores[largeBlobStoreIndex].length) {
3824+
if (setTotalLength > (short) currentLargeBlobStore.length) {
38083825
sendErrorByte(apdu, FIDOConstants.CTAP2_ERR_LARGE_BLOB_STORAGE_FULL);
38093826
}
38103827
} else {
@@ -3875,9 +3892,9 @@ private void handleLargeBlobs(APDU apdu, byte[] reqBuffer, short lc) {
38753892
*/
38763893
private void handleLargeBlobSet(APDU apdu, byte[] buffer, short incomingDataOffset,
38773894
short blobWriteOffset, short incomingDataLength, short totalLength) {
3878-
final byte tempBlobStoreIndex = (byte)(largeBlobStoreIndex == 0 ? 1 : 0);
3895+
final byte[] inactiveLargeBlobStore = getInactiveLargeBlobStore();
38793896
Util.arrayCopyNonAtomic(buffer, incomingDataOffset,
3880-
largeBlobStores[tempBlobStoreIndex], blobWriteOffset, incomingDataLength);
3897+
inactiveLargeBlobStore, blobWriteOffset, incomingDataLength);
38813898
// Done with incoming request now
38823899
bufferManager.informAPDUBufferAvailability(apdu, (short) 0xFF);
38833900

@@ -3890,22 +3907,23 @@ private void handleLargeBlobSet(APDU apdu, byte[] buffer, short incomingDataOffs
38903907
byte[] scratch = bufferManager.getBufferForHandle(apdu, scratchHandle);
38913908
short scratchOffset = bufferManager.getOffsetForHandle(scratchHandle);
38923909

3893-
sha256.doFinal(largeBlobStores[tempBlobStoreIndex], (short) 0, (short)(totalLength - 16),
3910+
sha256.doFinal(inactiveLargeBlobStore, (short) 0, (short)(totalLength - 16),
38943911
scratch, scratchOffset);
38953912

38963913
if (Util.arrayCompare(scratch, scratchOffset,
3897-
largeBlobStores[tempBlobStoreIndex], (short)(totalLength - 16), (short) 16) != 0) {
3914+
inactiveLargeBlobStore, (short)(totalLength - 16), (short) 16) != 0) {
38983915
// hash mismatch
38993916
sendErrorByte(apdu, FIDOConstants.CTAP2_ERR_INTEGRITY_FAILURE);
39003917
}
39013918

39023919
bufferManager.release(apdu, scratchHandle, (short) 32);
39033920

39043921
// Swapperoo the buffers
3922+
final byte newBlobStoreIndex = (byte)(largeBlobStoreIndex == 0 ? 1 : 0);
39053923
JCSystem.beginTransaction();
39063924
boolean ok = false;
39073925
try {
3908-
largeBlobStoreIndex = tempBlobStoreIndex;
3926+
largeBlobStoreIndex = newBlobStoreIndex;
39093927
largeBlobStoreFill = totalLength;
39103928
} finally {
39113929
if (ok) {
@@ -3952,7 +3970,7 @@ private void handleLargeBlobGet(APDU apdu, short numBytes, short offset) {
39523970
outBuffer[writeIdx++] = (byte) 0xA1; // map - one key
39533971
outBuffer[writeIdx++] = (byte) 0x01; // map key: config
39543972
writeIdx = encodeIntLenTo(outBuffer, writeIdx, bytesToRetrieve, true);
3955-
writeIdx = Util.arrayCopyNonAtomic(largeBlobStores[largeBlobStoreIndex], offset,
3973+
writeIdx = Util.arrayCopyNonAtomic(getCurrentLargeBlobStore(), offset,
39563974
outBuffer, writeIdx, bytesToRetrieve);
39573975

39583976
if (outBuffer == bufferMem) {
@@ -5374,10 +5392,12 @@ private void authenticatorReset(APDU apdu) {
53745392
final byte realBlobStoreIndex = largeBlobStoreIndex;
53755393

53765394
// Empty the large blob store OUTside the main transaction, since it's non-precious and double buffered
5377-
Util.arrayFillNonAtomic(largeBlobStores[tempBlobStoreIndex], (short) 0,
5378-
(short) largeBlobStores[tempBlobStoreIndex].length, (byte) 0x00);
5395+
final byte[] inactiveLargeBlobStore = getInactiveLargeBlobStore();
5396+
final byte[] activeLargeBlobStore = getCurrentLargeBlobStore();
5397+
Util.arrayFillNonAtomic(inactiveLargeBlobStore, (short) 0,
5398+
(short) inactiveLargeBlobStore.length, (byte) 0x00);
53795399
Util.arrayCopyNonAtomic(CannedCBOR.INITIAL_LARGE_BLOB_ARRAY, (short) 0,
5380-
largeBlobStores[tempBlobStoreIndex], (short) 0, (short) CannedCBOR.INITIAL_LARGE_BLOB_ARRAY.length);
5400+
inactiveLargeBlobStore, (short) 0, (short) CannedCBOR.INITIAL_LARGE_BLOB_ARRAY.length);
53815401
JCSystem.beginTransaction();
53825402
boolean ok = false;
53835403
try {
@@ -5391,10 +5411,10 @@ private void authenticatorReset(APDU apdu) {
53915411
JCSystem.abortTransaction();
53925412
}
53935413
}
5394-
Util.arrayFillNonAtomic(largeBlobStores[realBlobStoreIndex], (short) 0,
5395-
(short) largeBlobStores[realBlobStoreIndex].length, (byte) 0x00);
5414+
Util.arrayFillNonAtomic(activeLargeBlobStore, (short) 0,
5415+
(short) activeLargeBlobStore.length, (byte) 0x00);
53965416
Util.arrayCopyNonAtomic(CannedCBOR.INITIAL_LARGE_BLOB_ARRAY, (short) 0,
5397-
largeBlobStores[realBlobStoreIndex], (short) 0, (short) CannedCBOR.INITIAL_LARGE_BLOB_ARRAY.length);
5417+
activeLargeBlobStore, (short) 0, (short) CannedCBOR.INITIAL_LARGE_BLOB_ARRAY.length);
53985418

53995419
JCSystem.beginTransaction();
54005420
ok = false;
@@ -5594,7 +5614,7 @@ private void sendAuthInfo(APDU apdu) {
55945614

55955615
buffer[offset++] = 0x0B; // map key: maxSerializedLargeBlobArray: 1 byte = 5
55965616
buffer[offset++] = 0x19; // two-byte integer: 1 byte = 6
5597-
offset = Util.setShort(buffer, offset, (short) largeBlobStores[largeBlobStoreIndex].length); // 2 bytes = 8
5617+
offset = Util.setShort(buffer, offset, (short) getCurrentLargeBlobStore().length); // 2 bytes = 8
55985618

55995619
buffer[offset++] = 0x0C; // map key: forcePinChange: 1 byte = 9
56005620
buffer[offset++] = (byte)(forcePinChange ? 0xF5 : 0xF4); // 1 byte = 10
@@ -6818,11 +6838,12 @@ private FIDO2Applet(byte[] array, short offset, byte length) {
68186838
highSecurityWrappingIV = new byte[IV_LEN];
68196839
lowSecurityWrappingIV = new byte[IV_LEN];
68206840
externalCredentialIV = new byte[IV_LEN];
6821-
largeBlobStores = new byte[2][largeBlobStoreSize];
6841+
largeBlobStoreA = new byte[largeBlobStoreSize];
68226842
Util.arrayCopyNonAtomic(CannedCBOR.INITIAL_LARGE_BLOB_ARRAY, (short) 0,
6823-
largeBlobStores[0], (short) 0, (short) CannedCBOR.INITIAL_LARGE_BLOB_ARRAY.length);
6843+
largeBlobStoreA, (short) 0, (short) CannedCBOR.INITIAL_LARGE_BLOB_ARRAY.length);
6844+
largeBlobStoreB = new byte[largeBlobStoreSize];
68246845
Util.arrayCopyNonAtomic(CannedCBOR.INITIAL_LARGE_BLOB_ARRAY, (short) 0,
6825-
largeBlobStores[1], (short) 0, (short) CannedCBOR.INITIAL_LARGE_BLOB_ARRAY.length);
6846+
largeBlobStoreB, (short) 0, (short) CannedCBOR.INITIAL_LARGE_BLOB_ARRAY.length);
68266847
largeBlobStoreFill = (short) CannedCBOR.INITIAL_LARGE_BLOB_ARRAY.length;
68276848
highSecurityWrappingKey = getTransientAESKey(); // Our most important treasure, from which all other crypto is born...
68286849
lowSecurityWrappingKey = getPersistentAESKey(); // Not really a treasure

0 commit comments

Comments
 (0)