Skip to content

Commit 8fb9cd2

Browse files
Added byte encoding support to CredentialPublicKey
1 parent 40c1576 commit 8fb9cd2

File tree

4 files changed

+201
-77
lines changed

4 files changed

+201
-77
lines changed
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the WebAuthn Swift open source project
4+
//
5+
// Copyright (c) 2022 the WebAuthn Swift project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of WebAuthn Swift project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import Foundation
16+
import SwiftCBOR
17+
18+
extension CBOR {
19+
subscript(key: COSEKey) -> CBOR? {
20+
get { self[.signedInt(key)] }
21+
set { self[.signedInt(key)] = newValue }
22+
}
23+
24+
static func encodeSortedPairs(_ pairs: [(COSEKey, CBOR)], options: CBOROptions = CBOROptions()) -> [UInt8] {
25+
encodeSortedPairs(pairs.map { (CBOR.signedInt($0), $1) }, options: options)
26+
}
27+
}
28+
29+
extension CBOR {
30+
static func signedInt(_ int: some SignedInteger) -> CBOR {
31+
if int < 0 {
32+
return .negativeInt(UInt64(abs(-1 - int)))
33+
} else {
34+
return .unsignedInt(UInt64(int))
35+
}
36+
}
37+
38+
static func signedInt<T: RawRepresentable>(_ rawInt: T) -> CBOR where T.RawValue: SignedInteger {
39+
.signedInt(rawInt.rawValue)
40+
}
41+
42+
static func signedInt<T: RawRepresentable>(_ rawInt: T) -> CBOR where T.RawValue: UnsignedInteger {
43+
.unsignedInt(UInt64(rawInt.rawValue))
44+
}
45+
}
46+
47+
extension CBOR {
48+
/// Adapted from SwiftCBOR's ``CBOR/encodeMap(_:options:)`` to account for the [CTAP2 canonical CBOR encoding form](https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-errata-20220621.html#ctap2-canonical-cbor-encoding-form).
49+
static func encodeSortedPairs<Key: CBOREncodable, Value: CBOREncodable>(_ pairs: [(Key, Value)], options: CBOROptions = CBOROptions()) -> [UInt8] {
50+
var res: [UInt8] = []
51+
res.reserveCapacity(1 + pairs.count * (MemoryLayout<Key>.size + MemoryLayout<Value>.size + 2))
52+
res = pairs.count.encode(options: options)
53+
res[0] = res[0] | 0b101_00000
54+
for (k, v) in pairs {
55+
res.append(contentsOf: k.encode(options: options))
56+
res.append(contentsOf: v.encode(options: options))
57+
}
58+
return res
59+
}
60+
}
61+
62+
extension UnsignedInteger {
63+
init?(_ cbor: CBOR) {
64+
switch cbor {
65+
case .unsignedInt(let positiveInt):
66+
self = Self(positiveInt)
67+
default: return nil
68+
}
69+
}
70+
}
71+
72+
extension SignedInteger {
73+
init?(_ cbor: CBOR) {
74+
switch cbor {
75+
case .unsignedInt(let positiveInt):
76+
self = Self(positiveInt)
77+
case .negativeInt(let negativeInt):
78+
// https://github.com/unrelentingtech/SwiftCBOR#swiftcbor
79+
// Negative integers are decoded as NegativeInt(UInt), where the actual number is -1 - i
80+
self = -1 - Self(negativeInt)
81+
default: return nil
82+
}
83+
}
84+
}

Sources/WebAuthn/Ceremonies/Shared/COSE/COSEKey.swift

Lines changed: 15 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -12,47 +12,27 @@
1212
//
1313
//===----------------------------------------------------------------------===//
1414

15-
import SwiftCBOR
16-
17-
enum COSEKey: Sendable {
15+
struct COSEKey: RawRepresentable, Sendable {
16+
var rawValue: Int
17+
18+
init(rawValue: Int) {
19+
self.rawValue = rawValue
20+
}
21+
1822
// swiftlint:disable identifier_name
19-
case kty
20-
case alg
23+
static let kty = COSEKey(rawValue: 1)
24+
static let alg = COSEKey(rawValue: 3)
2125

2226
// EC2, OKP
23-
case crv
24-
case x
27+
static let crv = COSEKey(rawValue: -1)
28+
static let x = COSEKey(rawValue: -2)
2529

2630
// EC2
27-
case y
31+
static let y = COSEKey(rawValue: -3)
2832

2933
// RSA
30-
case n
31-
case e
34+
static let n = COSEKey(rawValue: -1)
35+
static let e = COSEKey(rawValue: -2)
3236
// swiftlint:enable identifier_name
33-
34-
var cbor: CBOR {
35-
var value: Int
36-
switch self {
37-
case .kty:
38-
value = 1
39-
case .alg:
40-
value = 3
41-
case .crv:
42-
value = -1
43-
case .x:
44-
value = -2
45-
case .y:
46-
value = -3
47-
case .n:
48-
value = -1
49-
case .e:
50-
value = -2
51-
}
52-
if value < 0 {
53-
return .negativeInt(UInt64(abs(-1 - value)))
54-
} else {
55-
return .unsignedInt(UInt64(value))
56-
}
57-
}
5837
}
38+

Sources/WebAuthn/Ceremonies/Shared/CredentialPublicKey.swift

Lines changed: 95 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -56,21 +56,14 @@ enum CredentialPublicKey: Sendable {
5656
return
5757
}
5858

59-
guard let keyTypeRaw = publicKeyObject[COSEKey.kty.cbor],
60-
case let .unsignedInt(keyTypeInt) = keyTypeRaw,
61-
let keyType = COSEKeyType(rawValue: keyTypeInt) else {
62-
throw WebAuthnError.invalidKeyType
63-
}
59+
guard let keyType = publicKeyObject[COSEKey.kty].flatMap(UInt64.init).flatMap(COSEKeyType.init(rawValue:))
60+
else { throw WebAuthnError.invalidKeyType }
6461

65-
guard let algorithmRaw = publicKeyObject[COSEKey.alg.cbor],
66-
case let .negativeInt(algorithmNegative) = algorithmRaw else {
67-
throw WebAuthnError.invalidAlgorithm
68-
}
69-
// https://github.com/unrelentingtech/SwiftCBOR#swiftcbor
70-
// Negative integers are decoded as NegativeInt(UInt), where the actual number is -1 - i
71-
guard let algorithm = COSEAlgorithmIdentifier(rawValue: -1 - Int(algorithmNegative)) else {
72-
throw WebAuthnError.unsupportedCOSEAlgorithm
73-
}
62+
guard let algorithmRaw = publicKeyObject[COSEKey.alg].flatMap(Int.init)
63+
else { throw WebAuthnError.invalidAlgorithm }
64+
65+
guard let algorithm = COSEAlgorithmIdentifier(rawValue: algorithmRaw)
66+
else { throw WebAuthnError.unsupportedCOSEAlgorithm }
7467

7568
// Currently we only support elliptic curve algorithms
7669
switch keyType {
@@ -89,6 +82,14 @@ enum CredentialPublicKey: Sendable {
8982
func verify(signature: some DataProtocol, data: some DataProtocol) throws {
9083
try key.verify(signature: signature, data: data)
9184
}
85+
86+
var bytes: [UInt8] {
87+
switch self {
88+
case .okp(let oKPPublicKey): oKPPublicKey.bytes
89+
case .ec2(let eC2PublicKey): eC2PublicKey.bytes
90+
case .rsa(let rSAPublicKeyData): rSAPublicKeyData.bytes
91+
}
92+
}
9293
}
9394

9495
struct EC2PublicKey: PublicKey, Sendable {
@@ -108,31 +109,72 @@ struct EC2PublicKey: PublicKey, Sendable {
108109
self.xCoordinate = xCoordinate
109110
self.yCoordinate = yCoordinate
110111
}
112+
113+
init(_ publicKey: P256.Signing.PublicKey) {
114+
self.algorithm = .algES256
115+
self.curve = .p256
116+
117+
/// Split the key like SwiftCrypto does internally: https://github.com/apple/swift-crypto/blob/606608da0875e3dee07cb37da3b38585420db111/Sources/Crypto/Signatures/ECDSA.swift.gyb#L79
118+
let rawRepresentation = publicKey.rawRepresentation
119+
let half = rawRepresentation.count/2
120+
self.xCoordinate = Array(rawRepresentation.prefix(half))
121+
self.yCoordinate = Array(rawRepresentation.suffix(half))
122+
}
123+
124+
init(_ publicKey: P384.Signing.PublicKey) {
125+
self.algorithm = .algES384
126+
self.curve = .p384
127+
128+
/// Split the key like SwiftCrypto does internally: https://github.com/apple/swift-crypto/blob/606608da0875e3dee07cb37da3b38585420db111/Sources/Crypto/Signatures/ECDSA.swift.gyb#L79
129+
let rawRepresentation = publicKey.rawRepresentation
130+
let half = rawRepresentation.count/2
131+
self.xCoordinate = Array(rawRepresentation.prefix(half))
132+
self.yCoordinate = Array(rawRepresentation.suffix(half))
133+
}
134+
135+
init(_ publicKey: P521.Signing.PublicKey) {
136+
self.algorithm = .algES512
137+
self.curve = .p521
138+
139+
/// Split the key like SwiftCrypto does internally: https://github.com/apple/swift-crypto/blob/606608da0875e3dee07cb37da3b38585420db111/Sources/Crypto/Signatures/ECDSA.swift.gyb#L79
140+
let rawRepresentation = publicKey.rawRepresentation
141+
let half = rawRepresentation.count/2
142+
self.xCoordinate = Array(rawRepresentation.prefix(half))
143+
self.yCoordinate = Array(rawRepresentation.suffix(half))
144+
}
111145

112146
init(publicKeyObject: CBOR, algorithm: COSEAlgorithmIdentifier) throws {
113147
self.algorithm = algorithm
114148

115149
// Curve is key -1 - or -0 for SwiftCBOR
116150
// X Coordinate is key -2, or NegativeInt 1 for SwiftCBOR
117151
// Y Coordinate is key -3, or NegativeInt 2 for SwiftCBOR
118-
guard let curveRaw = publicKeyObject[COSEKey.crv.cbor],
119-
case let .unsignedInt(curve) = curveRaw,
120-
let coseCurve = COSECurve(rawValue: curve) else {
121-
throw WebAuthnError.invalidCurve
122-
}
152+
guard let coseCurve = publicKeyObject[COSEKey.crv].flatMap(UInt64.init).flatMap(COSECurve.init(rawValue:))
153+
else { throw WebAuthnError.invalidCurve }
123154
self.curve = coseCurve
124155

125-
guard let xCoordRaw = publicKeyObject[COSEKey.x.cbor],
126-
case let .byteString(xCoordinateBytes) = xCoordRaw else {
127-
throw WebAuthnError.invalidXCoordinate
128-
}
156+
guard
157+
let xCoordRaw = publicKeyObject[COSEKey.x],
158+
case let .byteString(xCoordinateBytes) = xCoordRaw
159+
else { throw WebAuthnError.invalidXCoordinate }
129160
xCoordinate = xCoordinateBytes
130-
guard let yCoordRaw = publicKeyObject[COSEKey.y.cbor],
131-
case let .byteString(yCoordinateBytes) = yCoordRaw else {
132-
throw WebAuthnError.invalidYCoordinate
133-
}
161+
162+
guard
163+
let yCoordRaw = publicKeyObject[COSEKey.y],
164+
case let .byteString(yCoordinateBytes) = yCoordRaw
165+
else { throw WebAuthnError.invalidYCoordinate }
134166
yCoordinate = yCoordinateBytes
135167
}
168+
169+
var bytes: [UInt8] {
170+
CBOR.encodeSortedPairs([
171+
(COSEKey.kty, .signedInt(COSEKeyType.ellipticKey)),
172+
(COSEKey.alg, .signedInt(algorithm)),
173+
(COSEKey.crv, .signedInt(curve)),
174+
(COSEKey.x, .byteString(Array(xCoordinate))),
175+
(COSEKey.y, .byteString(Array(yCoordinate))),
176+
])
177+
}
136178

137179
func verify(signature: some DataProtocol, data: some DataProtocol) throws {
138180
switch algorithm {
@@ -171,18 +213,27 @@ struct RSAPublicKeyData: PublicKey, Sendable {
171213
init(publicKeyObject: CBOR, algorithm: COSEAlgorithmIdentifier) throws {
172214
self.algorithm = algorithm
173215

174-
guard let nRaw = publicKeyObject[COSEKey.n.cbor],
175-
case let .byteString(nBytes) = nRaw else {
176-
throw WebAuthnError.invalidModulus
177-
}
216+
guard
217+
let nRaw = publicKeyObject[COSEKey.n],
218+
case let .byteString(nBytes) = nRaw
219+
else { throw WebAuthnError.invalidModulus }
178220
n = nBytes
179221

180-
guard let eRaw = publicKeyObject[COSEKey.e.cbor],
181-
case let .byteString(eBytes) = eRaw else {
182-
throw WebAuthnError.invalidExponent
183-
}
222+
guard
223+
let eRaw = publicKeyObject[COSEKey.e],
224+
case let .byteString(eBytes) = eRaw
225+
else { throw WebAuthnError.invalidExponent }
184226
e = eBytes
185227
}
228+
229+
var bytes: [UInt8] {
230+
CBOR.encodeSortedPairs([
231+
(COSEKey.kty, .signedInt(COSEKeyType.rsaKey)),
232+
(COSEKey.alg, .signedInt(algorithm)),
233+
(COSEKey.n, .byteString(Array(n))),
234+
(COSEKey.e, .byteString(Array(e))),
235+
])
236+
}
186237

187238
func verify(signature: some DataProtocol, data: some DataProtocol) throws {
188239
throw WebAuthnError.unsupported
@@ -228,6 +279,15 @@ struct OKPPublicKey: PublicKey, Sendable {
228279
}
229280
xCoordinate = xCoordinateBytes
230281
}
282+
283+
var bytes: [UInt8] {
284+
CBOR.encodeSortedPairs([
285+
(COSEKey.kty, .signedInt(COSEKeyType.octetKey)),
286+
(COSEKey.alg, .signedInt(algorithm)),
287+
(COSEKey.crv, .unsignedInt(curve)),
288+
(COSEKey.x, .byteString(xCoordinate)),
289+
])
290+
}
231291

232292
func verify(signature: some DataProtocol, data: some DataProtocol) throws {
233293
throw WebAuthnError.unsupported

Tests/WebAuthnTests/Utils/TestModels/TestCredentialPublicKey.swift

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,23 +23,23 @@ struct TestCredentialPublicKey {
2323
var yCoordinate: CBOR?
2424

2525
var byteArrayRepresentation: [UInt8] {
26-
var value: [CBOR: CBOR] = [:]
26+
var value: [(COSEKey, CBOR)] = []
2727
if let kty {
28-
value[COSEKey.kty.cbor] = kty
28+
value.append((COSEKey.kty, kty))
2929
}
3030
if let alg {
31-
value[COSEKey.alg.cbor] = alg
31+
value.append((COSEKey.alg, alg))
3232
}
3333
if let crv {
34-
value[COSEKey.crv.cbor] = crv
34+
value.append((COSEKey.crv, crv))
3535
}
3636
if let xCoordinate {
37-
value[COSEKey.x.cbor] = xCoordinate
37+
value.append((COSEKey.x, xCoordinate))
3838
}
3939
if let yCoordinate {
40-
value[COSEKey.y.cbor] = yCoordinate
40+
value.append((COSEKey.y, yCoordinate))
4141
}
42-
return CBOR.map(value).encode()
42+
return CBOR.encodeSortedPairs(value)
4343
}
4444
}
4545

0 commit comments

Comments
 (0)