mirror of
https://github.com/go-gitea/gitea
synced 2025-12-07 13:28:25 +00:00
Support webauthn (#17957)
Migrate from U2F to Webauthn Co-authored-by: Andrew Thornton <art27@cantab.net> Co-authored-by: 6543 <6543@obermui.de> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
co-authored by
Andrew Thornton
6543
wxiaoguang
parent
8808293247
commit
35c3553870
+155
@@ -0,0 +1,155 @@
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/duo-labs/webauthn/protocol/webauthncose"
|
||||
)
|
||||
|
||||
// The raw response returned to us from an authenticator when we request a
|
||||
// credential for login/assertion.
|
||||
type CredentialAssertionResponse struct {
|
||||
PublicKeyCredential
|
||||
AssertionResponse AuthenticatorAssertionResponse `json:"response"`
|
||||
}
|
||||
|
||||
// The parsed CredentialAssertionResponse that has been marshalled into a format
|
||||
// that allows us to verify the client and authenticator data inside the response
|
||||
type ParsedCredentialAssertionData struct {
|
||||
ParsedPublicKeyCredential
|
||||
Response ParsedAssertionResponse
|
||||
Raw CredentialAssertionResponse
|
||||
}
|
||||
|
||||
// The AuthenticatorAssertionResponse contains the raw authenticator assertion data and is parsed into
|
||||
// ParsedAssertionResponse
|
||||
type AuthenticatorAssertionResponse struct {
|
||||
AuthenticatorResponse
|
||||
AuthenticatorData URLEncodedBase64 `json:"authenticatorData"`
|
||||
Signature URLEncodedBase64 `json:"signature"`
|
||||
UserHandle URLEncodedBase64 `json:"userHandle,omitempty"`
|
||||
}
|
||||
|
||||
// Parsed form of AuthenticatorAssertionResponse
|
||||
type ParsedAssertionResponse struct {
|
||||
CollectedClientData CollectedClientData
|
||||
AuthenticatorData AuthenticatorData
|
||||
Signature []byte
|
||||
UserHandle []byte
|
||||
}
|
||||
|
||||
// Parse the credential request response into a format that is either required by the specification
|
||||
// or makes the assertion verification steps easier to complete. This takes an http.Request that contains
|
||||
// the assertion response data in a raw, mostly base64 encoded format, and parses the data into
|
||||
// manageable structures
|
||||
func ParseCredentialRequestResponse(response *http.Request) (*ParsedCredentialAssertionData, error) {
|
||||
if response == nil || response.Body == nil {
|
||||
return nil, ErrBadRequest.WithDetails("No response given")
|
||||
}
|
||||
return ParseCredentialRequestResponseBody(response.Body)
|
||||
}
|
||||
|
||||
// Parse the credential request response into a format that is either required by the specification
|
||||
// or makes the assertion verification steps easier to complete. This takes an io.Reader that contains
|
||||
// the assertion response data in a raw, mostly base64 encoded format, and parses the data into
|
||||
// manageable structures
|
||||
func ParseCredentialRequestResponseBody(body io.Reader) (*ParsedCredentialAssertionData, error) {
|
||||
var car CredentialAssertionResponse
|
||||
err := json.NewDecoder(body).Decode(&car)
|
||||
if err != nil {
|
||||
return nil, ErrBadRequest.WithDetails("Parse error for Assertion")
|
||||
}
|
||||
|
||||
if car.ID == "" {
|
||||
return nil, ErrBadRequest.WithDetails("CredentialAssertionResponse with ID missing")
|
||||
}
|
||||
|
||||
_, err = base64.RawURLEncoding.DecodeString(car.ID)
|
||||
if err != nil {
|
||||
return nil, ErrBadRequest.WithDetails("CredentialAssertionResponse with ID not base64url encoded")
|
||||
}
|
||||
if car.Type != "public-key" {
|
||||
return nil, ErrBadRequest.WithDetails("CredentialAssertionResponse with bad type")
|
||||
}
|
||||
var par ParsedCredentialAssertionData
|
||||
par.ID, par.RawID, par.Type, par.ClientExtensionResults = car.ID, car.RawID, car.Type, car.ClientExtensionResults
|
||||
par.Raw = car
|
||||
|
||||
par.Response.Signature = car.AssertionResponse.Signature
|
||||
par.Response.UserHandle = car.AssertionResponse.UserHandle
|
||||
|
||||
// Step 5. Let JSONtext be the result of running UTF-8 decode on the value of cData.
|
||||
// We don't call it cData but this is Step 5 in the spec.
|
||||
err = json.Unmarshal(car.AssertionResponse.ClientDataJSON, &par.Response.CollectedClientData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = par.Response.AuthenticatorData.Unmarshal(car.AssertionResponse.AuthenticatorData)
|
||||
if err != nil {
|
||||
return nil, ErrParsingData.WithDetails("Error unmarshalling auth data")
|
||||
}
|
||||
return &par, nil
|
||||
}
|
||||
|
||||
// Follow the remaining steps outlined in §7.2 Verifying an authentication assertion
|
||||
// (https://www.w3.org/TR/webauthn/#verifying-assertion) and return an error if there
|
||||
// is a failure during each step.
|
||||
func (p *ParsedCredentialAssertionData) Verify(storedChallenge string, relyingPartyID, relyingPartyOrigin, appID string, verifyUser bool, credentialBytes []byte) error {
|
||||
// Steps 4 through 6 in verifying the assertion data (https://www.w3.org/TR/webauthn/#verifying-assertion) are
|
||||
// "assertive" steps, i.e "Let JSONtext be the result of running UTF-8 decode on the value of cData."
|
||||
// We handle these steps in part as we verify but also beforehand
|
||||
|
||||
// Handle steps 7 through 10 of assertion by verifying stored data against the Collected Client Data
|
||||
// returned by the authenticator
|
||||
validError := p.Response.CollectedClientData.Verify(storedChallenge, AssertCeremony, relyingPartyOrigin)
|
||||
if validError != nil {
|
||||
return validError
|
||||
}
|
||||
|
||||
// Begin Step 11. Verify that the rpIdHash in authData is the SHA-256 hash of the RP ID expected by the RP.
|
||||
rpIDHash := sha256.Sum256([]byte(relyingPartyID))
|
||||
|
||||
var appIDHash [32]byte
|
||||
if appID != "" {
|
||||
appIDHash = sha256.Sum256([]byte(appID))
|
||||
}
|
||||
|
||||
// Handle steps 11 through 14, verifying the authenticator data.
|
||||
validError = p.Response.AuthenticatorData.Verify(rpIDHash[:], appIDHash[:], verifyUser)
|
||||
if validError != nil {
|
||||
return ErrAuthData.WithInfo(validError.Error())
|
||||
}
|
||||
|
||||
// allowedUserCredentialIDs := session.AllowedCredentialIDs
|
||||
|
||||
// Step 15. Let hash be the result of computing a hash over the cData using SHA-256.
|
||||
clientDataHash := sha256.Sum256(p.Raw.AssertionResponse.ClientDataJSON)
|
||||
|
||||
// Step 16. Using the credential public key looked up in step 3, verify that sig is
|
||||
// a valid signature over the binary concatenation of authData and hash.
|
||||
|
||||
sigData := append(p.Raw.AssertionResponse.AuthenticatorData, clientDataHash[:]...)
|
||||
|
||||
var (
|
||||
key interface{}
|
||||
err error
|
||||
)
|
||||
|
||||
if appID == "" {
|
||||
key, err = webauthncose.ParsePublicKey(credentialBytes)
|
||||
} else {
|
||||
key, err = webauthncose.ParseFIDOPublicKey(credentialBytes)
|
||||
}
|
||||
|
||||
valid, err := webauthncose.VerifySignature(key, sigData, p.Response.Signature)
|
||||
if !valid {
|
||||
return ErrAssertionSignature.WithDetails(fmt.Sprintf("Error validating the assertion signature: %+v\n", err))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+156
@@ -0,0 +1,156 @@
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/fxamacker/cbor/v2"
|
||||
)
|
||||
|
||||
// From §5.2.1 (https://www.w3.org/TR/webauthn/#authenticatorattestationresponse)
|
||||
// "The authenticator's response to a client’s request for the creation
|
||||
// of a new public key credential. It contains information about the new credential
|
||||
// that can be used to identify it for later use, and metadata that can be used by
|
||||
// the WebAuthn Relying Party to assess the characteristics of the credential
|
||||
// during registration."
|
||||
|
||||
// The initial unpacked 'response' object received by the relying party. This
|
||||
// contains the clientDataJSON object, which will be marshalled into
|
||||
// CollectedClientData, and the 'attestationObject', which contains
|
||||
// information about the authenticator, and the newly minted
|
||||
// public key credential. The information in both objects are used
|
||||
// to verify the authenticity of the ceremony and new credential
|
||||
type AuthenticatorAttestationResponse struct {
|
||||
// The byte slice of clientDataJSON, which becomes CollectedClientData
|
||||
AuthenticatorResponse
|
||||
// The byte slice version of AttestationObject
|
||||
// This attribute contains an attestation object, which is opaque to, and
|
||||
// cryptographically protected against tampering by, the client. The
|
||||
// attestation object contains both authenticator data and an attestation
|
||||
// statement. The former contains the AAGUID, a unique credential ID, and
|
||||
// the credential public key. The contents of the attestation statement are
|
||||
// determined by the attestation statement format used by the authenticator.
|
||||
// It also contains any additional information that the Relying Party's server
|
||||
// requires to validate the attestation statement, as well as to decode and
|
||||
// validate the authenticator data along with the JSON-serialized client data.
|
||||
AttestationObject URLEncodedBase64 `json:"attestationObject"`
|
||||
}
|
||||
|
||||
// The parsed out version of AuthenticatorAttestationResponse.
|
||||
type ParsedAttestationResponse struct {
|
||||
CollectedClientData CollectedClientData
|
||||
AttestationObject AttestationObject
|
||||
}
|
||||
|
||||
// From §6.4. Authenticators MUST also provide some form of attestation. The basic requirement is that the
|
||||
// authenticator can produce, for each credential public key, an attestation statement verifiable by the
|
||||
// WebAuthn Relying Party. Typically, this attestation statement contains a signature by an attestation
|
||||
// private key over the attested credential public key and a challenge, as well as a certificate or similar
|
||||
// data providing provenance information for the attestation public key, enabling the Relying Party to make
|
||||
// a trust decision. However, if an attestation key pair is not available, then the authenticator MUST
|
||||
// perform self attestation of the credential public key with the corresponding credential private key.
|
||||
// All this information is returned by authenticators any time a new public key credential is generated, in
|
||||
// the overall form of an attestation object. (https://www.w3.org/TR/webauthn/#attestation-object)
|
||||
//
|
||||
type AttestationObject struct {
|
||||
// The authenticator data, including the newly created public key. See AuthenticatorData for more info
|
||||
AuthData AuthenticatorData
|
||||
// The byteform version of the authenticator data, used in part for signature validation
|
||||
RawAuthData []byte `json:"authData"`
|
||||
// The format of the Attestation data.
|
||||
Format string `json:"fmt"`
|
||||
// The attestation statement data sent back if attestation is requested.
|
||||
AttStatement map[string]interface{} `json:"attStmt,omitempty"`
|
||||
}
|
||||
|
||||
type attestationFormatValidationHandler func(AttestationObject, []byte) (string, []interface{}, error)
|
||||
|
||||
var attestationRegistry = make(map[string]attestationFormatValidationHandler)
|
||||
|
||||
// Using one of the locally registered attestation formats, handle validating the attestation
|
||||
// data provided by the authenticator (and in some cases its manufacturer)
|
||||
func RegisterAttestationFormat(format string, handler attestationFormatValidationHandler) {
|
||||
attestationRegistry[format] = handler
|
||||
}
|
||||
|
||||
// Parse the values returned in the authenticator response and perform attestation verification
|
||||
// Step 8. This returns a fully decoded struct with the data put into a format that can be
|
||||
// used to verify the user and credential that was created
|
||||
func (ccr *AuthenticatorAttestationResponse) Parse() (*ParsedAttestationResponse, error) {
|
||||
var p ParsedAttestationResponse
|
||||
|
||||
err := json.Unmarshal(ccr.ClientDataJSON, &p.CollectedClientData)
|
||||
if err != nil {
|
||||
return nil, ErrParsingData.WithInfo(err.Error())
|
||||
}
|
||||
|
||||
err = cbor.Unmarshal(ccr.AttestationObject, &p.AttestationObject)
|
||||
if err != nil {
|
||||
return nil, ErrParsingData.WithInfo(err.Error())
|
||||
}
|
||||
|
||||
// Step 8. Perform CBOR decoding on the attestationObject field of the AuthenticatorAttestationResponse
|
||||
// structure to obtain the attestation statement format fmt, the authenticator data authData, and
|
||||
// the attestation statement attStmt.
|
||||
err = p.AttestationObject.AuthData.Unmarshal(p.AttestationObject.RawAuthData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error decoding auth data: %v", err)
|
||||
}
|
||||
|
||||
if !p.AttestationObject.AuthData.Flags.HasAttestedCredentialData() {
|
||||
return nil, ErrAttestationFormat.WithInfo("Attestation missing attested credential data flag")
|
||||
}
|
||||
|
||||
return &p, nil
|
||||
}
|
||||
|
||||
// Verify - Perform Steps 9 through 14 of registration verification, delegating Steps
|
||||
func (attestationObject *AttestationObject) Verify(relyingPartyID string, clientDataHash []byte, verificationRequired bool) error {
|
||||
// Steps 9 through 12 are verified against the auth data.
|
||||
// These steps are identical to 11 through 14 for assertion
|
||||
// so we handle them with AuthData
|
||||
|
||||
// Begin Step 9. Verify that the rpIdHash in authData is
|
||||
// the SHA-256 hash of the RP ID expected by the RP.
|
||||
rpIDHash := sha256.Sum256([]byte(relyingPartyID))
|
||||
// Handle Steps 9 through 12
|
||||
authDataVerificationError := attestationObject.AuthData.Verify(rpIDHash[:], nil, verificationRequired)
|
||||
if authDataVerificationError != nil {
|
||||
return authDataVerificationError
|
||||
}
|
||||
|
||||
// Step 13. Determine the attestation statement format by performing a
|
||||
// USASCII case-sensitive match on fmt against the set of supported
|
||||
// WebAuthn Attestation Statement Format Identifier values. The up-to-date
|
||||
// list of registered WebAuthn Attestation Statement Format Identifier
|
||||
// values is maintained in the IANA registry of the same name
|
||||
// [WebAuthn-Registries] (https://www.w3.org/TR/webauthn/#biblio-webauthn-registries).
|
||||
|
||||
// Since there is not an active registry yet, we'll check it against our internal
|
||||
// Supported types.
|
||||
|
||||
// But first let's make sure attestation is present. If it isn't, we don't need to handle
|
||||
// any of the following steps
|
||||
if attestationObject.Format == "none" {
|
||||
if len(attestationObject.AttStatement) != 0 {
|
||||
return ErrAttestationFormat.WithInfo("Attestation format none with attestation present")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
formatHandler, valid := attestationRegistry[attestationObject.Format]
|
||||
if !valid {
|
||||
return ErrAttestationFormat.WithInfo(fmt.Sprintf("Attestation format %s is unsupported", attestationObject.Format))
|
||||
}
|
||||
|
||||
// Step 14. Verify that attStmt is a correct attestation statement, conveying a valid attestation signature, by using
|
||||
// the attestation statement format fmt’s verification procedure given attStmt, authData and the hash of the serialized
|
||||
// client data computed in step 7.
|
||||
attestationType, _, err := formatHandler(*attestationObject, clientDataHash)
|
||||
if err != nil {
|
||||
return err.(*Error).WithInfo(attestationType)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
+227
@@ -0,0 +1,227 @@
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/x509"
|
||||
"encoding/asn1"
|
||||
"fmt"
|
||||
|
||||
"github.com/duo-labs/webauthn/protocol/webauthncose"
|
||||
)
|
||||
|
||||
var androidAttestationKey = "android-key"
|
||||
|
||||
func init() {
|
||||
RegisterAttestationFormat(androidAttestationKey, verifyAndroidKeyFormat)
|
||||
}
|
||||
|
||||
// From §8.4. https://www.w3.org/TR/webauthn/#android-key-attestation
|
||||
// The android-key attestation statement looks like:
|
||||
// $$attStmtType //= (
|
||||
// fmt: "android-key",
|
||||
// attStmt: androidStmtFormat
|
||||
// )
|
||||
// androidStmtFormat = {
|
||||
// alg: COSEAlgorithmIdentifier,
|
||||
// sig: bytes,
|
||||
// x5c: [ credCert: bytes, * (caCert: bytes) ]
|
||||
// }
|
||||
func verifyAndroidKeyFormat(att AttestationObject, clientDataHash []byte) (string, []interface{}, error) {
|
||||
// Given the verification procedure inputs attStmt, authenticatorData and clientDataHash, the verification procedure is as follows:
|
||||
// §8.4.1. Verify that attStmt is valid CBOR conforming to the syntax defined above and perform CBOR decoding on it to extract
|
||||
// the contained fields.
|
||||
|
||||
// Get the alg value - A COSEAlgorithmIdentifier containing the identifier of the algorithm
|
||||
// used to generate the attestation signature.
|
||||
alg, present := att.AttStatement["alg"].(int64)
|
||||
if !present {
|
||||
return androidAttestationKey, nil, ErrAttestationFormat.WithDetails("Error retreiving alg value")
|
||||
}
|
||||
|
||||
// Get the sig value - A byte string containing the attestation signature.
|
||||
sig, present := att.AttStatement["sig"].([]byte)
|
||||
if !present {
|
||||
return androidAttestationKey, nil, ErrAttestationFormat.WithDetails("Error retreiving sig value")
|
||||
}
|
||||
|
||||
// If x5c is not present, return an error
|
||||
x5c, x509present := att.AttStatement["x5c"].([]interface{})
|
||||
if !x509present {
|
||||
// Handle Basic Attestation steps for the x509 Certificate
|
||||
return androidAttestationKey, nil, ErrAttestationFormat.WithDetails("Error retreiving x5c value")
|
||||
}
|
||||
|
||||
// §8.4.2. Verify that sig is a valid signature over the concatenation of authenticatorData and clientDataHash
|
||||
// using the public key in the first certificate in x5c with the algorithm specified in alg.
|
||||
attCertBytes, valid := x5c[0].([]byte)
|
||||
if !valid {
|
||||
return androidAttestationKey, nil, ErrAttestation.WithDetails("Error getting certificate from x5c cert chain")
|
||||
}
|
||||
|
||||
signatureData := append(att.RawAuthData, clientDataHash...)
|
||||
|
||||
attCert, err := x509.ParseCertificate(attCertBytes)
|
||||
if err != nil {
|
||||
return androidAttestationKey, nil, ErrAttestationFormat.WithDetails(fmt.Sprintf("Error parsing certificate from ASN.1 data: %+v", err))
|
||||
}
|
||||
|
||||
coseAlg := webauthncose.COSEAlgorithmIdentifier(alg)
|
||||
sigAlg := webauthncose.SigAlgFromCOSEAlg(coseAlg)
|
||||
err = attCert.CheckSignature(x509.SignatureAlgorithm(sigAlg), signatureData, sig)
|
||||
if err != nil {
|
||||
return androidAttestationKey, nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Signature validation error: %+v\n", err))
|
||||
}
|
||||
// Verify that the public key in the first certificate in x5c matches the credentialPublicKey in the attestedCredentialData in authenticatorData.
|
||||
pubKey, err := webauthncose.ParsePublicKey(att.AuthData.AttData.CredentialPublicKey)
|
||||
if err != nil {
|
||||
return androidAttestationKey, nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Error parsing public key: %+v\n", err))
|
||||
}
|
||||
e := pubKey.(webauthncose.EC2PublicKeyData)
|
||||
valid, err = e.Verify(signatureData, sig)
|
||||
if err != nil || valid != true {
|
||||
return androidAttestationKey, nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Error parsing public key: %+v\n", err))
|
||||
}
|
||||
// §8.4.3. Verify that the attestationChallenge field in the attestation certificate extension data is identical to clientDataHash.
|
||||
// attCert.Extensions
|
||||
var attExtBytes []byte
|
||||
for _, ext := range attCert.Extensions {
|
||||
if ext.Id.Equal([]int{1, 3, 6, 1, 4, 1, 11129, 2, 1, 17}) {
|
||||
attExtBytes = ext.Value
|
||||
}
|
||||
}
|
||||
if len(attExtBytes) == 0 {
|
||||
return androidAttestationKey, nil, ErrAttestationFormat.WithDetails("Attestation certificate extensions missing 1.3.6.1.4.1.11129.2.1.17")
|
||||
}
|
||||
// As noted in §8.4.1 (https://w3c.github.io/webauthn/#key-attstn-cert-requirements) the Android Key Attestation attestation certificate's
|
||||
// android key attestation certificate extension data is identified by the OID "1.3.6.1.4.1.11129.2.1.17".
|
||||
decoded := keyDescription{}
|
||||
_, err = asn1.Unmarshal([]byte(attExtBytes), &decoded)
|
||||
if err != nil {
|
||||
return androidAttestationKey, nil, ErrAttestationFormat.WithDetails("Unable to parse Android key attestation certificate extensions")
|
||||
}
|
||||
// Verify that the attestationChallenge field in the attestation certificate extension data is identical to clientDataHash.
|
||||
if 0 != bytes.Compare(decoded.AttestationChallenge, clientDataHash) {
|
||||
return androidAttestationKey, nil, ErrAttestationFormat.WithDetails("Attestation challenge not equal to clientDataHash")
|
||||
}
|
||||
// The AuthorizationList.allApplications field is not present on either authorization list (softwareEnforced nor teeEnforced), since PublicKeyCredential MUST be scoped to the RP ID.
|
||||
if nil != decoded.SoftwareEnforced.AllApplications || nil != decoded.TeeEnforced.AllApplications {
|
||||
return androidAttestationKey, nil, ErrAttestationFormat.WithDetails("Attestation certificate extensions contains all applications field")
|
||||
}
|
||||
// For the following, use only the teeEnforced authorization list if the RP wants to accept only keys from a trusted execution environment, otherwise use the union of teeEnforced and softwareEnforced.
|
||||
// The value in the AuthorizationList.origin field is equal to KM_ORIGIN_GENERATED. (which == 0)
|
||||
if KM_ORIGIN_GENERATED != decoded.SoftwareEnforced.Origin || KM_ORIGIN_GENERATED != decoded.TeeEnforced.Origin {
|
||||
return androidAttestationKey, nil, ErrAttestationFormat.WithDetails("Attestation certificate extensions contains authorization list with origin not equal KM_ORIGIN_GENERATED")
|
||||
}
|
||||
// The value in the AuthorizationList.purpose field is equal to KM_PURPOSE_SIGN. (which == 2)
|
||||
if !contains(decoded.SoftwareEnforced.Purpose, KM_PURPOSE_SIGN) && !contains(decoded.TeeEnforced.Purpose, KM_PURPOSE_SIGN) {
|
||||
return androidAttestationKey, nil, ErrAttestationFormat.WithDetails("Attestation certificate extensions contains authorization list with purpose not equal KM_PURPOSE_SIGN")
|
||||
}
|
||||
return androidAttestationKey, x5c, err
|
||||
}
|
||||
|
||||
func contains(s []int, e int) bool {
|
||||
for _, a := range s {
|
||||
if a == e {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type keyDescription struct {
|
||||
AttestationVersion int
|
||||
AttestationSecurityLevel asn1.Enumerated
|
||||
KeymasterVersion int
|
||||
KeymasterSecurityLevel asn1.Enumerated
|
||||
AttestationChallenge []byte
|
||||
UniqueID []byte
|
||||
SoftwareEnforced authorizationList
|
||||
TeeEnforced authorizationList
|
||||
}
|
||||
|
||||
type authorizationList struct {
|
||||
Purpose []int `asn1:"tag:1,explicit,set,optional"`
|
||||
Algorithm int `asn1:"tag:2,explicit,optional"`
|
||||
KeySize int `asn1:"tag:3,explicit,optional"`
|
||||
Digest []int `asn1:"tag:5,explicit,set,optional"`
|
||||
Padding []int `asn1:"tag:6,explicit,set,optional"`
|
||||
EcCurve int `asn1:"tag:10,explicit,optional"`
|
||||
RsaPublicExponent int `asn1:"tag:200,explicit,optional"`
|
||||
RollbackResistance interface{} `asn1:"tag:303,explicit,optional"`
|
||||
ActiveDateTime int `asn1:"tag:400,explicit,optional"`
|
||||
OriginationExpireDateTime int `asn1:"tag:401,explicit,optional"`
|
||||
UsageExpireDateTime int `asn1:"tag:402,explicit,optional"`
|
||||
NoAuthRequired interface{} `asn1:"tag:503,explicit,optional"`
|
||||
UserAuthType int `asn1:"tag:504,explicit,optional"`
|
||||
AuthTimeout int `asn1:"tag:505,explicit,optional"`
|
||||
AllowWhileOnBody interface{} `asn1:"tag:506,explicit,optional"`
|
||||
TrustedUserPresenceRequired interface{} `asn1:"tag:507,explicit,optional"`
|
||||
TrustedConfirmationRequired interface{} `asn1:"tag:508,explicit,optional"`
|
||||
UnlockedDeviceRequired interface{} `asn1:"tag:509,explicit,optional"`
|
||||
AllApplications interface{} `asn1:"tag:600,explicit,optional"`
|
||||
ApplicationID interface{} `asn1:"tag:601,explicit,optional"`
|
||||
CreationDateTime int `asn1:"tag:701,explicit,optional"`
|
||||
Origin int `asn1:"tag:702,explicit,optional"`
|
||||
RootOfTrust rootOfTrust `asn1:"tag:704,explicit,optional"`
|
||||
OsVersion int `asn1:"tag:705,explicit,optional"`
|
||||
OsPatchLevel int `asn1:"tag:706,explicit,optional"`
|
||||
AttestationApplicationID []byte `asn1:"tag:709,explicit,optional"`
|
||||
AttestationIDBrand []byte `asn1:"tag:710,explicit,optional"`
|
||||
AttestationIDDevice []byte `asn1:"tag:711,explicit,optional"`
|
||||
AttestationIDProduct []byte `asn1:"tag:712,explicit,optional"`
|
||||
AttestationIDSerial []byte `asn1:"tag:713,explicit,optional"`
|
||||
AttestationIDImei []byte `asn1:"tag:714,explicit,optional"`
|
||||
AttestationIDMeid []byte `asn1:"tag:715,explicit,optional"`
|
||||
AttestationIDManufacturer []byte `asn1:"tag:716,explicit,optional"`
|
||||
AttestationIDModel []byte `asn1:"tag:717,explicit,optional"`
|
||||
VendorPatchLevel int `asn1:"tag:718,explicit,optional"`
|
||||
BootPatchLevel int `asn1:"tag:719,explicit,optional"`
|
||||
}
|
||||
|
||||
type rootOfTrust struct {
|
||||
verifiedBootKey []byte
|
||||
deviceLocked bool
|
||||
verifiedBootState verifiedBootState
|
||||
verifiedBootHash []byte
|
||||
}
|
||||
|
||||
type verifiedBootState int
|
||||
|
||||
const (
|
||||
Verified verifiedBootState = iota
|
||||
SelfSigned
|
||||
Unverified
|
||||
Failed
|
||||
)
|
||||
|
||||
/**
|
||||
* The origin of a key (or pair), i.e. where it was generated. Note that KM_TAG_ORIGIN can be found
|
||||
* in either the hardware-enforced or software-enforced list for a key, indicating whether the key
|
||||
* is hardware or software-based. Specifically, a key with KM_ORIGIN_GENERATED in the
|
||||
* hardware-enforced list is guaranteed never to have existed outide the secure hardware.
|
||||
*/
|
||||
type KM_KEY_ORIGIN int
|
||||
|
||||
const (
|
||||
KM_ORIGIN_GENERATED = iota /* Generated in keymaster. Should not exist outside the TEE. */
|
||||
KM_ORIGIN_DERIVED /* Derived inside keymaster. Likely exists off-device. */
|
||||
KM_ORIGIN_IMPORTED /* Imported into keymaster. Existed as cleartext in Android. */
|
||||
KM_ORIGIN_UNKNOWN /* Keymaster did not record origin. This value can only be seen on
|
||||
* keys in a keymaster0 implementation. The keymaster0 adapter uses
|
||||
* this value to document the fact that it is unkown whether the key
|
||||
* was generated inside or imported into keymaster. */
|
||||
)
|
||||
|
||||
/**
|
||||
* Possible purposes of a key (or pair).
|
||||
*/
|
||||
type KM_PURPOSE int
|
||||
|
||||
const (
|
||||
KM_PURPOSE_ENCRYPT = iota /* Usable with RSA, EC and AES keys. */
|
||||
KM_PURPOSE_DECRYPT /* Usable with RSA, EC and AES keys. */
|
||||
KM_PURPOSE_SIGN /* Usable with RSA, EC and HMAC keys. */
|
||||
KM_PURPOSE_VERIFY /* Usable with RSA, EC and HMAC keys. */
|
||||
KM_PURPOSE_DERIVE_KEY /* Usable with EC keys. */
|
||||
KM_PURPOSE_WRAP /* Usable with wrapped keys. */
|
||||
)
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"encoding/asn1"
|
||||
"fmt"
|
||||
"math/big"
|
||||
|
||||
"github.com/duo-labs/webauthn/protocol/webauthncose"
|
||||
)
|
||||
|
||||
var appleAttestationKey = "apple"
|
||||
|
||||
func init() {
|
||||
RegisterAttestationFormat(appleAttestationKey, verifyAppleKeyFormat)
|
||||
}
|
||||
|
||||
// From §8.8. https://www.w3.org/TR/webauthn-2/#sctn-apple-anonymous-attestation
|
||||
// The apple attestation statement looks like:
|
||||
// $$attStmtType //= (
|
||||
// fmt: "apple",
|
||||
// attStmt: appleStmtFormat
|
||||
// )
|
||||
// appleStmtFormat = {
|
||||
// x5c: [ credCert: bytes, * (caCert: bytes) ]
|
||||
// }
|
||||
func verifyAppleKeyFormat(att AttestationObject, clientDataHash []byte) (string, []interface{}, error) {
|
||||
|
||||
// Step 1. Verify that attStmt is valid CBOR conforming to the syntax defined
|
||||
// above and perform CBOR decoding on it to extract the contained fields.
|
||||
|
||||
// If x5c is not present, return an error
|
||||
x5c, x509present := att.AttStatement["x5c"].([]interface{})
|
||||
if !x509present {
|
||||
// Handle Basic Attestation steps for the x509 Certificate
|
||||
return appleAttestationKey, nil, ErrAttestationFormat.WithDetails("Error retreiving x5c value")
|
||||
}
|
||||
|
||||
credCertBytes, valid := x5c[0].([]byte)
|
||||
if !valid {
|
||||
return appleAttestationKey, nil, ErrAttestation.WithDetails("Error getting certificate from x5c cert chain")
|
||||
}
|
||||
|
||||
credCert, err := x509.ParseCertificate(credCertBytes)
|
||||
if err != nil {
|
||||
return appleAttestationKey, nil, ErrAttestationFormat.WithDetails(fmt.Sprintf("Error parsing certificate from ASN.1 data: %+v", err))
|
||||
}
|
||||
|
||||
// Step 2. Concatenate authenticatorData and clientDataHash to form nonceToHash.
|
||||
nonceToHash := append(att.RawAuthData, clientDataHash...)
|
||||
|
||||
// Step 3. Perform SHA-256 hash of nonceToHash to produce nonce.
|
||||
nonce := sha256.Sum256(nonceToHash)
|
||||
|
||||
// Step 4. Verify that nonce equals the value of the extension with OID 1.2.840.113635.100.8.2 in credCert.
|
||||
var attExtBytes []byte
|
||||
for _, ext := range credCert.Extensions {
|
||||
if ext.Id.Equal([]int{1, 2, 840, 113635, 100, 8, 2}) {
|
||||
attExtBytes = ext.Value
|
||||
}
|
||||
}
|
||||
if len(attExtBytes) == 0 {
|
||||
return appleAttestationKey, nil, ErrAttestationFormat.WithDetails("Attestation certificate extensions missing 1.2.840.113635.100.8.2")
|
||||
}
|
||||
|
||||
decoded := AppleAnonymousAttestation{}
|
||||
_, err = asn1.Unmarshal([]byte(attExtBytes), &decoded)
|
||||
if err != nil {
|
||||
return appleAttestationKey, nil, ErrAttestationFormat.WithDetails("Unable to parse apple attestation certificate extensions")
|
||||
}
|
||||
|
||||
if !bytes.Equal(decoded.Nonce, nonce[:]) || err != nil {
|
||||
return appleAttestationKey, nil, ErrInvalidAttestation.WithDetails("Attestation certificate does not contain expected nonce")
|
||||
}
|
||||
|
||||
// Step 5. Verify that the credential public key equals the Subject Public Key of credCert.
|
||||
// TODO: Probably move this part to webauthncose.go
|
||||
pubKey, err := webauthncose.ParsePublicKey(att.AuthData.AttData.CredentialPublicKey)
|
||||
if err != nil {
|
||||
return appleAttestationKey, nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Error parsing public key: %+v\n", err))
|
||||
}
|
||||
credPK := pubKey.(webauthncose.EC2PublicKeyData)
|
||||
subjectPK := credCert.PublicKey.(*ecdsa.PublicKey)
|
||||
credPKInfo := &ecdsa.PublicKey{
|
||||
Curve: elliptic.P256(),
|
||||
X: big.NewInt(0).SetBytes(credPK.XCoord),
|
||||
Y: big.NewInt(0).SetBytes(credPK.YCoord),
|
||||
}
|
||||
if !credPKInfo.Equal(subjectPK) {
|
||||
return appleAttestationKey, nil, ErrInvalidAttestation.WithDetails("Certificate public key does not match public key in authData")
|
||||
}
|
||||
|
||||
// Step 6. If successful, return implementation-specific values representing attestation type Anonymization CA and attestation trust path x5c.
|
||||
return appleAttestationKey, x5c, nil
|
||||
}
|
||||
|
||||
// Apple has not yet publish schema for the extension(as of JULY 2021.)
|
||||
type AppleAnonymousAttestation struct {
|
||||
Nonce []byte `asn1:"tag:1,explicit"`
|
||||
}
|
||||
+278
@@ -0,0 +1,278 @@
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/x509"
|
||||
"encoding/asn1"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/duo-labs/webauthn/metadata"
|
||||
uuid "github.com/satori/go.uuid"
|
||||
|
||||
"github.com/duo-labs/webauthn/protocol/webauthncose"
|
||||
)
|
||||
|
||||
var packedAttestationKey = "packed"
|
||||
|
||||
func init() {
|
||||
RegisterAttestationFormat(packedAttestationKey, verifyPackedFormat)
|
||||
}
|
||||
|
||||
// From §8.2. https://www.w3.org/TR/webauthn/#packed-attestation
|
||||
// The packed attestation statement looks like:
|
||||
// packedStmtFormat = {
|
||||
// alg: COSEAlgorithmIdentifier,
|
||||
// sig: bytes,
|
||||
// x5c: [ attestnCert: bytes, * (caCert: bytes) ]
|
||||
// } OR
|
||||
// {
|
||||
// alg: COSEAlgorithmIdentifier, (-260 for ED256 / -261 for ED512)
|
||||
// sig: bytes,
|
||||
// ecdaaKeyId: bytes
|
||||
// } OR
|
||||
// {
|
||||
// alg: COSEAlgorithmIdentifier
|
||||
// sig: bytes,
|
||||
// }
|
||||
func verifyPackedFormat(att AttestationObject, clientDataHash []byte) (string, []interface{}, error) {
|
||||
// Step 1. Verify that attStmt is valid CBOR conforming to the syntax defined
|
||||
// above and perform CBOR decoding on it to extract the contained fields.
|
||||
|
||||
// Get the alg value - A COSEAlgorithmIdentifier containing the identifier of the algorithm
|
||||
// used to generate the attestation signature.
|
||||
|
||||
alg, present := att.AttStatement["alg"].(int64)
|
||||
if !present {
|
||||
return packedAttestationKey, nil, ErrAttestationFormat.WithDetails("Error retreiving alg value")
|
||||
}
|
||||
|
||||
// Get the sig value - A byte string containing the attestation signature.
|
||||
sig, present := att.AttStatement["sig"].([]byte)
|
||||
if !present {
|
||||
return packedAttestationKey, nil, ErrAttestationFormat.WithDetails("Error retreiving sig value")
|
||||
}
|
||||
|
||||
// Step 2. If x5c is present, this indicates that the attestation type is not ECDAA.
|
||||
x5c, x509present := att.AttStatement["x5c"].([]interface{})
|
||||
if x509present {
|
||||
// Handle Basic Attestation steps for the x509 Certificate
|
||||
return handleBasicAttestation(sig, clientDataHash, att.RawAuthData, att.AuthData.AttData.AAGUID, alg, x5c)
|
||||
}
|
||||
|
||||
// Step 3. If ecdaaKeyId is present, then the attestation type is ECDAA.
|
||||
// Also make sure the we did not have an x509 then
|
||||
ecdaaKeyID, ecdaaKeyPresent := att.AttStatement["ecdaaKeyId"].([]byte)
|
||||
if ecdaaKeyPresent {
|
||||
// Handle ECDAA Attestation steps for the x509 Certificate
|
||||
return handleECDAAAttesation(sig, clientDataHash, ecdaaKeyID)
|
||||
}
|
||||
|
||||
// Step 4. If neither x5c nor ecdaaKeyId is present, self attestation is in use.
|
||||
return handleSelfAttestation(alg, att.AuthData.AttData.CredentialPublicKey, att.RawAuthData, clientDataHash, sig)
|
||||
}
|
||||
|
||||
// Handle the attestation steps laid out in
|
||||
func handleBasicAttestation(signature, clientDataHash, authData, aaguid []byte, alg int64, x5c []interface{}) (string, []interface{}, error) {
|
||||
// Step 2.1. Verify that sig is a valid signature over the concatenation of authenticatorData
|
||||
// and clientDataHash using the attestation public key in attestnCert with the algorithm specified in alg.
|
||||
attestationType := "Packed (Basic)"
|
||||
|
||||
for _, c := range x5c {
|
||||
cb, cv := c.([]byte)
|
||||
if !cv {
|
||||
return attestationType, x5c, ErrAttestation.WithDetails("Error getting certificate from x5c cert chain")
|
||||
}
|
||||
ct, err := x509.ParseCertificate(cb)
|
||||
if err != nil {
|
||||
return attestationType, x5c, ErrAttestationFormat.WithDetails(fmt.Sprintf("Error parsing certificate from ASN.1 data: %+v", err))
|
||||
}
|
||||
if ct.NotBefore.After(time.Now()) || ct.NotAfter.Before(time.Now()) {
|
||||
return attestationType, x5c, ErrAttestationFormat.WithDetails("Cert in chain not time valid")
|
||||
}
|
||||
}
|
||||
|
||||
attCertBytes, valid := x5c[0].([]byte)
|
||||
if !valid {
|
||||
return attestationType, x5c, ErrAttestation.WithDetails("Error getting certificate from x5c cert chain")
|
||||
}
|
||||
|
||||
signatureData := append(authData, clientDataHash...)
|
||||
|
||||
attCert, err := x509.ParseCertificate(attCertBytes)
|
||||
if err != nil {
|
||||
return attestationType, x5c, ErrAttestationFormat.WithDetails(fmt.Sprintf("Error parsing certificate from ASN.1 data: %+v", err))
|
||||
}
|
||||
|
||||
coseAlg := webauthncose.COSEAlgorithmIdentifier(alg)
|
||||
sigAlg := webauthncose.SigAlgFromCOSEAlg(coseAlg)
|
||||
err = attCert.CheckSignature(x509.SignatureAlgorithm(sigAlg), signatureData, signature)
|
||||
if err != nil {
|
||||
return attestationType, x5c, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Signature validation error: %+v\n", err))
|
||||
}
|
||||
|
||||
// Step 2.2 Verify that attestnCert meets the requirements in §8.2.1 Packed attestation statement certificate requirements.
|
||||
// §8.2.1 can be found here https://www.w3.org/TR/webauthn/#packed-attestation-cert-requirements
|
||||
|
||||
// Step 2.2.1 (from §8.2.1) Version MUST be set to 3 (which is indicated by an ASN.1 INTEGER with value 2).
|
||||
if attCert.Version != 3 {
|
||||
return attestationType, x5c, ErrAttestationCertificate.WithDetails("Attestation Certificate is incorrect version")
|
||||
}
|
||||
|
||||
// Step 2.2.2 (from §8.2.1) Subject field MUST be set to:
|
||||
|
||||
// Subject-C
|
||||
// ISO 3166 code specifying the country where the Authenticator vendor is incorporated (PrintableString)
|
||||
|
||||
// TODO: Find a good, useable, country code library. For now, check stringy-ness
|
||||
subjectString := strings.Join(attCert.Subject.Country, "")
|
||||
if subjectString == "" {
|
||||
return attestationType, x5c, ErrAttestationCertificate.WithDetails("Attestation Certificate Country Code is invalid")
|
||||
}
|
||||
|
||||
// Subject-O
|
||||
// Legal name of the Authenticator vendor (UTF8String)
|
||||
subjectString = strings.Join(attCert.Subject.Organization, "")
|
||||
if subjectString == "" {
|
||||
return attestationType, x5c, ErrAttestationCertificate.WithDetails("Attestation Certificate Organization is invalid")
|
||||
}
|
||||
|
||||
// Subject-OU
|
||||
// Literal string “Authenticator Attestation” (UTF8String)
|
||||
subjectString = strings.Join(attCert.Subject.OrganizationalUnit, " ")
|
||||
if subjectString != "Authenticator Attestation" {
|
||||
// TODO: Implement a return error when I'm more certain this is general practice
|
||||
}
|
||||
|
||||
// Subject-CN
|
||||
// A UTF8String of the vendor’s choosing
|
||||
subjectString = attCert.Subject.CommonName
|
||||
if subjectString == "" {
|
||||
return attestationType, x5c, ErrAttestationCertificate.WithDetails("Attestation Certificate Common Name not set")
|
||||
}
|
||||
// TODO: And then what
|
||||
|
||||
// Step 2.2.3 (from §8.2.1) If the related attestation root certificate is used for multiple authenticator models,
|
||||
// the Extension OID 1.3.6.1.4.1.45724.1.1.4 (id-fido-gen-ce-aaguid) MUST be present, containing the
|
||||
// AAGUID as a 16-byte OCTET STRING. The extension MUST NOT be marked as critical.
|
||||
|
||||
idFido := asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 45724, 1, 1, 4}
|
||||
var foundAAGUID []byte
|
||||
for _, extension := range attCert.Extensions {
|
||||
if extension.Id.Equal(idFido) {
|
||||
if extension.Critical {
|
||||
return attestationType, x5c, ErrInvalidAttestation.WithDetails("Attestation certificate FIDO extension marked as critical")
|
||||
}
|
||||
foundAAGUID = extension.Value
|
||||
}
|
||||
}
|
||||
|
||||
// We validate the AAGUID as mentioned above
|
||||
// This is not well defined in§8.2.1 but mentioned in step 2.3: we validate the AAGUID if it is present within the certificate
|
||||
// and make sure it matches the auth data AAGUID
|
||||
// Note that an X.509 Extension encodes the DER-encoding of the value in an OCTET STRING. Thus, the
|
||||
// AAGUID MUST be wrapped in two OCTET STRINGS to be valid.
|
||||
if len(foundAAGUID) > 0 {
|
||||
unMarshalledAAGUID := []byte{}
|
||||
asn1.Unmarshal(foundAAGUID, &unMarshalledAAGUID)
|
||||
if !bytes.Equal(aaguid, unMarshalledAAGUID) {
|
||||
return attestationType, x5c, ErrInvalidAttestation.WithDetails("Certificate AAGUID does not match Auth Data certificate")
|
||||
}
|
||||
}
|
||||
uuid, err := uuid.FromBytes(aaguid)
|
||||
|
||||
if meta, ok := metadata.Metadata[uuid]; ok {
|
||||
for _, s := range meta.StatusReports {
|
||||
if metadata.IsUndesiredAuthenticatorStatus(metadata.AuthenticatorStatus(s.Status)) {
|
||||
return attestationType, x5c, ErrInvalidAttestation.WithDetails("Authenticator with undesirable status encountered")
|
||||
}
|
||||
}
|
||||
|
||||
if attCert.Subject.CommonName != attCert.Issuer.CommonName {
|
||||
var hasBasicFull = false
|
||||
for _, a := range meta.MetadataStatement.AttestationTypes {
|
||||
if metadata.AuthenticatorAttestationType(a) == metadata.AuthenticatorAttestationType(metadata.BasicFull) {
|
||||
hasBasicFull = true
|
||||
}
|
||||
}
|
||||
if !hasBasicFull {
|
||||
return attestationType, x5c, ErrInvalidAttestation.WithDetails("Attestation with full attestation from authentictor that does not support full attestation")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if metadata.Conformance {
|
||||
return attestationType, x5c, ErrInvalidAttestation.WithDetails("AAGUID not found in metadata during conformance testing")
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2.2.4 The Basic Constraints extension MUST have the CA component set to false.
|
||||
if attCert.IsCA {
|
||||
return attestationType, x5c, ErrInvalidAttestation.WithDetails("Attestation certificate's Basic Constraints marked as CA")
|
||||
}
|
||||
|
||||
// Note for 2.2.5 An Authority Information Access (AIA) extension with entry id-ad-ocsp and a CRL
|
||||
// Distribution Point extension [RFC5280](https://www.w3.org/TR/webauthn/#biblio-rfc5280) are
|
||||
// both OPTIONAL as the status of many attestation certificates is available through authenticator
|
||||
// metadata services. See, for example, the FIDO Metadata Service
|
||||
// [FIDOMetadataService] (https://www.w3.org/TR/webauthn/#biblio-fidometadataservice)
|
||||
|
||||
// Step 2.4 If successful, return attestation type Basic and attestation trust path x5c.
|
||||
// We don't handle trust paths yet but we're done
|
||||
return attestationType, x5c, nil
|
||||
}
|
||||
|
||||
func handleECDAAAttesation(signature, clientDataHash, ecdaaKeyID []byte) (string, []interface{}, error) {
|
||||
return "Packed (ECDAA)", nil, ErrNotSpecImplemented
|
||||
}
|
||||
|
||||
func handleSelfAttestation(alg int64, pubKey, authData, clientDataHash, signature []byte) (string, []interface{}, error) {
|
||||
attestationType := "Packed (Self)"
|
||||
// §4.1 Validate that alg matches the algorithm of the credentialPublicKey in authenticatorData.
|
||||
|
||||
// §4.2 Verify that sig is a valid signature over the concatenation of authenticatorData and
|
||||
// clientDataHash using the credential public key with alg.
|
||||
verificationData := append(authData, clientDataHash...)
|
||||
|
||||
key, err := webauthncose.ParsePublicKey(pubKey)
|
||||
if err != nil {
|
||||
return attestationType, nil, ErrAttestationFormat.WithDetails(fmt.Sprintf("Error parsing the public key: %+v\n", err))
|
||||
}
|
||||
|
||||
switch key.(type) {
|
||||
case webauthncose.OKPPublicKeyData:
|
||||
k := key.(webauthncose.OKPPublicKeyData)
|
||||
err := verifyKeyAlgorithm(k.Algorithm, alg)
|
||||
if err != nil {
|
||||
return attestationType, nil, err
|
||||
}
|
||||
case webauthncose.EC2PublicKeyData:
|
||||
k := key.(webauthncose.EC2PublicKeyData)
|
||||
err := verifyKeyAlgorithm(k.Algorithm, alg)
|
||||
if err != nil {
|
||||
return attestationType, nil, err
|
||||
}
|
||||
case webauthncose.RSAPublicKeyData:
|
||||
k := key.(webauthncose.RSAPublicKeyData)
|
||||
err := verifyKeyAlgorithm(k.Algorithm, alg)
|
||||
if err != nil {
|
||||
return attestationType, nil, err
|
||||
}
|
||||
default:
|
||||
return attestationType, nil, ErrInvalidAttestation.WithDetails("Error verifying the public key data")
|
||||
}
|
||||
|
||||
valid, err := webauthncose.VerifySignature(key, verificationData, signature)
|
||||
if !valid && err == nil {
|
||||
return attestationType, nil, ErrInvalidAttestation.WithDetails("Unabled to verify signature")
|
||||
}
|
||||
|
||||
return attestationType, nil, err
|
||||
}
|
||||
|
||||
func verifyKeyAlgorithm(keyAlgorithm, attestedAlgorithm int64) error {
|
||||
if keyAlgorithm != attestedAlgorithm {
|
||||
return ErrInvalidAttestation.WithDetails("Public key algorithm does not equal att statement algorithm")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/duo-labs/webauthn/metadata"
|
||||
|
||||
jwt "github.com/golang-jwt/jwt/v4"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
)
|
||||
|
||||
var safetyNetAttestationKey = "android-safetynet"
|
||||
|
||||
func init() {
|
||||
RegisterAttestationFormat(safetyNetAttestationKey, verifySafetyNetFormat)
|
||||
}
|
||||
|
||||
type SafetyNetResponse struct {
|
||||
Nonce string `json:"nonce"`
|
||||
TimestampMs int64 `json:"timestampMs"`
|
||||
ApkPackageName string `json:"apkPackageName"`
|
||||
ApkDigestSha256 string `json:"apkDigestSha256"`
|
||||
CtsProfileMatch bool `json:"ctsProfileMatch"`
|
||||
ApkCertificateDigestSha256 []interface{} `json:"apkCertificateDigestSha256"`
|
||||
BasicIntegrity bool `json:"basicIntegrity"`
|
||||
}
|
||||
|
||||
// Thanks to @koesie10 and @herrjemand for outlining how to support this type really well
|
||||
|
||||
// §8.5. Android SafetyNet Attestation Statement Format https://w3c.github.io/webauthn/#android-safetynet-attestation
|
||||
// When the authenticator in question is a platform-provided Authenticator on certain Android platforms, the attestation
|
||||
// statement is based on the SafetyNet API. In this case the authenticator data is completely controlled by the caller of
|
||||
// the SafetyNet API (typically an application running on the Android platform) and the attestation statement only provides
|
||||
// some statements about the health of the platform and the identity of the calling application. This attestation does not
|
||||
// provide information regarding provenance of the authenticator and its associated data. Therefore platform-provided
|
||||
// authenticators SHOULD make use of the Android Key Attestation when available, even if the SafetyNet API is also present.
|
||||
func verifySafetyNetFormat(att AttestationObject, clientDataHash []byte) (string, []interface{}, error) {
|
||||
// The syntax of an Android Attestation statement is defined as follows:
|
||||
// $$attStmtType //= (
|
||||
// fmt: "android-safetynet",
|
||||
// attStmt: safetynetStmtFormat
|
||||
// )
|
||||
|
||||
// safetynetStmtFormat = {
|
||||
// ver: text,
|
||||
// response: bytes
|
||||
// }
|
||||
|
||||
// §8.5.1 Verify that attStmt is valid CBOR conforming to the syntax defined above and perform CBOR decoding on it to extract
|
||||
// the contained fields.
|
||||
|
||||
// We have done this
|
||||
// §8.5.2 Verify that response is a valid SafetyNet response of version ver.
|
||||
version, present := att.AttStatement["ver"].(string)
|
||||
if !present {
|
||||
return safetyNetAttestationKey, nil, ErrAttestationFormat.WithDetails("Unable to find the version of SafetyNet")
|
||||
}
|
||||
|
||||
if version == "" {
|
||||
return safetyNetAttestationKey, nil, ErrAttestationFormat.WithDetails("Not a proper version for SafetyNet")
|
||||
}
|
||||
|
||||
// TODO: provide user the ability to designate their supported versions
|
||||
|
||||
response, present := att.AttStatement["response"].([]byte)
|
||||
if !present {
|
||||
return safetyNetAttestationKey, nil, ErrAttestationFormat.WithDetails("Unable to find the SafetyNet response")
|
||||
}
|
||||
|
||||
token, err := jwt.Parse(string(response), func(token *jwt.Token) (interface{}, error) {
|
||||
chain := token.Header["x5c"].([]interface{})
|
||||
o := make([]byte, base64.StdEncoding.DecodedLen(len(chain[0].(string))))
|
||||
n, err := base64.StdEncoding.Decode(o, []byte(chain[0].(string)))
|
||||
cert, err := x509.ParseCertificate(o[:n])
|
||||
return cert.PublicKey, err
|
||||
})
|
||||
if err != nil {
|
||||
return safetyNetAttestationKey, nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Error finding cert issued to correct hostname: %+v", err))
|
||||
}
|
||||
|
||||
// marshall the JWT payload into the safetynet response json
|
||||
var safetyNetResponse SafetyNetResponse
|
||||
err = mapstructure.Decode(token.Claims, &safetyNetResponse)
|
||||
if err != nil {
|
||||
return safetyNetAttestationKey, nil, ErrAttestationFormat.WithDetails(fmt.Sprintf("Error parsing the SafetyNet response: %+v", err))
|
||||
}
|
||||
|
||||
// §8.5.3 Verify that the nonce in the response is identical to the Base64 encoding of the SHA-256 hash of the concatenation
|
||||
// of authenticatorData and clientDataHash.
|
||||
nonceBuffer := sha256.Sum256(append(att.RawAuthData, clientDataHash...))
|
||||
nonceBytes, err := base64.StdEncoding.DecodeString(safetyNetResponse.Nonce)
|
||||
if !bytes.Equal(nonceBuffer[:], nonceBytes) || err != nil {
|
||||
return safetyNetAttestationKey, nil, ErrInvalidAttestation.WithDetails("Invalid nonce for in SafetyNet response")
|
||||
}
|
||||
|
||||
// §8.5.4 Let attestationCert be the attestation certificate (https://www.w3.org/TR/webauthn/#attestation-certificate)
|
||||
certChain := token.Header["x5c"].([]interface{})
|
||||
l := make([]byte, base64.StdEncoding.DecodedLen(len(certChain[0].(string))))
|
||||
n, err := base64.StdEncoding.Decode(l, []byte(certChain[0].(string)))
|
||||
if err != nil {
|
||||
return safetyNetAttestationKey, nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Error finding cert issued to correct hostname: %+v", err))
|
||||
}
|
||||
attestationCert, err := x509.ParseCertificate(l[:n])
|
||||
if err != nil {
|
||||
return safetyNetAttestationKey, nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Error finding cert issued to correct hostname: %+v", err))
|
||||
}
|
||||
|
||||
// §8.5.5 Verify that attestationCert is issued to the hostname "attest.android.com"
|
||||
err = attestationCert.VerifyHostname("attest.android.com")
|
||||
if err != nil {
|
||||
return safetyNetAttestationKey, nil, ErrInvalidAttestation.WithDetails(fmt.Sprintf("Error finding cert issued to correct hostname: %+v", err))
|
||||
}
|
||||
|
||||
// §8.5.6 Verify that the ctsProfileMatch attribute in the payload of response is true.
|
||||
if !safetyNetResponse.CtsProfileMatch {
|
||||
return safetyNetAttestationKey, nil, ErrInvalidAttestation.WithDetails("ctsProfileMatch attribute of the JWT payload is false")
|
||||
}
|
||||
|
||||
// Verify sanity of timestamp in the payload
|
||||
now := time.Now()
|
||||
oneMinuteAgo := now.Add(-time.Minute)
|
||||
t := time.Unix(safetyNetResponse.TimestampMs/1000, 0)
|
||||
if t.After(now) {
|
||||
// zero tolerance for post-dated timestamps
|
||||
return "Basic attestation with SafetyNet", nil, ErrInvalidAttestation.WithDetails("SafetyNet response with timestamp after current time")
|
||||
} else if t.Before(oneMinuteAgo) {
|
||||
// allow old timestamp for testing purposes
|
||||
// TODO: Make this user configurable
|
||||
msg := "SafetyNet response with timestamp before one minute ago"
|
||||
if metadata.Conformance {
|
||||
return "Basic attestation with SafetyNet", nil, ErrInvalidAttestation.WithDetails(msg)
|
||||
}
|
||||
}
|
||||
|
||||
// §8.5.7 If successful, return implementation-specific values representing attestation type Basic and attestation
|
||||
// trust path attestationCert.
|
||||
return "Basic attestation with SafetyNet", nil, nil
|
||||
}
|
||||
+349
@@ -0,0 +1,349 @@
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/asn1"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"strings"
|
||||
|
||||
"github.com/duo-labs/webauthn/protocol/webauthncose"
|
||||
|
||||
"github.com/duo-labs/webauthn/protocol/googletpm"
|
||||
)
|
||||
|
||||
var tpmAttestationKey = "tpm"
|
||||
|
||||
func init() {
|
||||
RegisterAttestationFormat(tpmAttestationKey, verifyTPMFormat)
|
||||
googletpm.UseTPM20LengthPrefixSize()
|
||||
}
|
||||
|
||||
func verifyTPMFormat(att AttestationObject, clientDataHash []byte) (string, []interface{}, error) {
|
||||
// Given the verification procedure inputs attStmt, authenticatorData
|
||||
// and clientDataHash, the verification procedure is as follows
|
||||
|
||||
// Verify that attStmt is valid CBOR conforming to the syntax defined
|
||||
// above and perform CBOR decoding on it to extract the contained fields
|
||||
|
||||
ver, present := att.AttStatement["ver"].(string)
|
||||
if !present {
|
||||
return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("Error retreiving ver value")
|
||||
}
|
||||
|
||||
if ver != "2.0" {
|
||||
return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("WebAuthn only supports TPM 2.0 currently")
|
||||
}
|
||||
|
||||
alg, present := att.AttStatement["alg"].(int64)
|
||||
if !present {
|
||||
return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("Error retreiving alg value")
|
||||
}
|
||||
|
||||
coseAlg := webauthncose.COSEAlgorithmIdentifier(alg)
|
||||
|
||||
x5c, x509present := att.AttStatement["x5c"].([]interface{})
|
||||
if !x509present {
|
||||
// Handle Basic Attestation steps for the x509 Certificate
|
||||
return tpmAttestationKey, nil, ErrNotImplemented
|
||||
}
|
||||
|
||||
_, ecdaaKeyPresent := att.AttStatement["ecdaaKeyId"].([]byte)
|
||||
if ecdaaKeyPresent {
|
||||
return tpmAttestationKey, nil, ErrNotImplemented
|
||||
}
|
||||
|
||||
sigBytes, present := att.AttStatement["sig"].([]byte)
|
||||
if !present {
|
||||
return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("Error retreiving sig value")
|
||||
}
|
||||
|
||||
certInfoBytes, present := att.AttStatement["certInfo"].([]byte)
|
||||
if !present {
|
||||
return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("Error retreiving certInfo value")
|
||||
}
|
||||
|
||||
pubAreaBytes, present := att.AttStatement["pubArea"].([]byte)
|
||||
if !present {
|
||||
return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("Error retreiving pubArea value")
|
||||
}
|
||||
|
||||
// Verify that the public key specified by the parameters and unique fields of pubArea
|
||||
// is identical to the credentialPublicKey in the attestedCredentialData in authenticatorData.
|
||||
pubArea, err := googletpm.DecodePublic(pubAreaBytes)
|
||||
if err != nil {
|
||||
return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("Unable to decode TPMT_PUBLIC in attestation statement")
|
||||
}
|
||||
|
||||
key, err := webauthncose.ParsePublicKey(att.AuthData.AttData.CredentialPublicKey)
|
||||
switch key.(type) {
|
||||
case webauthncose.EC2PublicKeyData:
|
||||
e := key.(webauthncose.EC2PublicKeyData)
|
||||
if pubArea.ECCParameters.CurveID != googletpm.EllipticCurve(e.Curve) ||
|
||||
0 != pubArea.ECCParameters.Point.X.Cmp(new(big.Int).SetBytes(e.XCoord)) ||
|
||||
0 != pubArea.ECCParameters.Point.Y.Cmp(new(big.Int).SetBytes(e.YCoord)) {
|
||||
return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("Mismatch between ECCParameters in pubArea and credentialPublicKey")
|
||||
}
|
||||
case webauthncose.RSAPublicKeyData:
|
||||
r := key.(webauthncose.RSAPublicKeyData)
|
||||
mod := new(big.Int).SetBytes(r.Modulus)
|
||||
exp := uint32(r.Exponent[0]) + uint32(r.Exponent[1])<<8 + uint32(r.Exponent[2])<<16
|
||||
if 0 != pubArea.RSAParameters.Modulus.Cmp(mod) ||
|
||||
pubArea.RSAParameters.Exponent != exp {
|
||||
return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("Mismatch between RSAParameters in pubArea and credentialPublicKey")
|
||||
}
|
||||
default:
|
||||
return "", nil, ErrUnsupportedKey
|
||||
}
|
||||
|
||||
// Concatenate authenticatorData and clientDataHash to form attToBeSigned
|
||||
attToBeSigned := append(att.RawAuthData, clientDataHash...)
|
||||
|
||||
// Validate that certInfo is valid:
|
||||
certInfo, err := googletpm.DecodeAttestationData(certInfoBytes)
|
||||
// 1/4 Verify that magic is set to TPM_GENERATED_VALUE.
|
||||
if certInfo.Magic != 0xff544347 {
|
||||
return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("Magic is not set to TPM_GENERATED_VALUE")
|
||||
}
|
||||
// 2/4 Verify that type is set to TPM_ST_ATTEST_CERTIFY.
|
||||
if certInfo.Type != googletpm.TagAttestCertify {
|
||||
return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("Type is not set to TPM_ST_ATTEST_CERTIFY")
|
||||
}
|
||||
// 3/4 Verify that extraData is set to the hash of attToBeSigned using the hash algorithm employed in "alg".
|
||||
f := webauthncose.HasherFromCOSEAlg(coseAlg)
|
||||
h := f()
|
||||
h.Write(attToBeSigned)
|
||||
if 0 != bytes.Compare(certInfo.ExtraData, h.Sum(nil)) {
|
||||
return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("ExtraData is not set to hash of attToBeSigned")
|
||||
}
|
||||
// 4/4 Verify that attested contains a TPMS_CERTIFY_INFO structure as specified in
|
||||
// [TPMv2-Part2] section 10.12.3, whose name field contains a valid Name for pubArea,
|
||||
// as computed using the algorithm in the nameAlg field of pubArea
|
||||
// using the procedure specified in [TPMv2-Part1] section 16.
|
||||
f, err = certInfo.AttestedCertifyInfo.Name.Digest.Alg.HashConstructor()
|
||||
h = f()
|
||||
h.Write(pubAreaBytes)
|
||||
if 0 != bytes.Compare(h.Sum(nil), certInfo.AttestedCertifyInfo.Name.Digest.Value) {
|
||||
return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("Hash value mismatch attested and pubArea")
|
||||
}
|
||||
|
||||
// Note that the remaining fields in the "Standard Attestation Structure"
|
||||
// [TPMv2-Part1] section 31.2, i.e., qualifiedSigner, clockInfo and firmwareVersion
|
||||
// are ignored. These fields MAY be used as an input to risk engines.
|
||||
|
||||
// If x5c is present, this indicates that the attestation type is not ECDAA.
|
||||
if x509present {
|
||||
// In this case:
|
||||
// Verify the sig is a valid signature over certInfo using the attestation public key in aikCert with the algorithm specified in alg.
|
||||
aikCertBytes, valid := x5c[0].([]byte)
|
||||
if !valid {
|
||||
return tpmAttestationKey, nil, ErrAttestation.WithDetails("Error getting certificate from x5c cert chain")
|
||||
}
|
||||
|
||||
aikCert, err := x509.ParseCertificate(aikCertBytes)
|
||||
if err != nil {
|
||||
return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("Error parsing certificate from ASN.1")
|
||||
}
|
||||
|
||||
sigAlg := webauthncose.SigAlgFromCOSEAlg(coseAlg)
|
||||
|
||||
err = aikCert.CheckSignature(x509.SignatureAlgorithm(sigAlg), certInfoBytes, sigBytes)
|
||||
if err != nil {
|
||||
return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails(fmt.Sprintf("Signature validation error: %+v\n", err))
|
||||
}
|
||||
// Verify that aikCert meets the requirements in §8.3.1 TPM Attestation Statement Certificate Requirements
|
||||
|
||||
// 1/6 Version MUST be set to 3.
|
||||
if aikCert.Version != 3 {
|
||||
return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("AIK certificate version must be 3")
|
||||
}
|
||||
// 2/6 Subject field MUST be set to empty.
|
||||
if aikCert.Subject.String() != "" {
|
||||
return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("AIK certificate subject must be empty")
|
||||
}
|
||||
|
||||
// 3/6 The Subject Alternative Name extension MUST be set as defined in [TPMv2-EK-Profile] section 3.2.9{}
|
||||
var manufacturer, model, version string
|
||||
for _, ext := range aikCert.Extensions {
|
||||
if ext.Id.Equal([]int{2, 5, 29, 17}) {
|
||||
manufacturer, model, version, err = parseSANExtension(ext.Value)
|
||||
}
|
||||
}
|
||||
|
||||
if manufacturer == "" || model == "" || version == "" {
|
||||
return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("Invalid SAN data in AIK certificate")
|
||||
}
|
||||
|
||||
if false == isValidTPMManufacturer(manufacturer) {
|
||||
return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("Invalid TPM manufacturer")
|
||||
}
|
||||
|
||||
// 4/6 The Extended Key Usage extension MUST contain the "joint-iso-itu-t(2) internationalorganizations(23) 133 tcg-kp(8) tcg-kp-AIKCertificate(3)" OID.
|
||||
var ekuValid = false
|
||||
var eku []asn1.ObjectIdentifier
|
||||
for _, ext := range aikCert.Extensions {
|
||||
if ext.Id.Equal([]int{2, 5, 29, 37}) {
|
||||
rest, err := asn1.Unmarshal(ext.Value, &eku)
|
||||
if len(rest) != 0 || err != nil || !eku[0].Equal(tcgKpAIKCertificate) {
|
||||
return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("AIK certificate EKU missing 2.23.133.8.3")
|
||||
}
|
||||
ekuValid = true
|
||||
}
|
||||
}
|
||||
if false == ekuValid {
|
||||
return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("AIK certificate missing EKU")
|
||||
}
|
||||
|
||||
// 5/6 The Basic Constraints extension MUST have the CA component set to false.
|
||||
type basicConstraints struct {
|
||||
IsCA bool `asn1:"optional"`
|
||||
MaxPathLen int `asn1:"optional,default:-1"`
|
||||
}
|
||||
var constraints basicConstraints
|
||||
for _, ext := range aikCert.Extensions {
|
||||
if ext.Id.Equal([]int{2, 5, 29, 19}) {
|
||||
if rest, err := asn1.Unmarshal(ext.Value, &constraints); err != nil {
|
||||
return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("AIK certificate basic constraints malformed")
|
||||
} else if len(rest) != 0 {
|
||||
return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("AIK certificate basic constraints contains extra data")
|
||||
}
|
||||
}
|
||||
}
|
||||
if constraints.IsCA != false {
|
||||
return tpmAttestationKey, nil, ErrAttestationFormat.WithDetails("AIK certificate basic constraints missing or CA is true")
|
||||
}
|
||||
// 6/6 An Authority Information Access (AIA) extension with entry id-ad-ocsp and a CRL Distribution Point
|
||||
// extension [RFC5280] are both OPTIONAL as the status of many attestation certificates is available
|
||||
// through metadata services. See, for example, the FIDO Metadata Service.
|
||||
}
|
||||
|
||||
return tpmAttestationKey, x5c, err
|
||||
}
|
||||
func forEachSAN(extension []byte, callback func(tag int, data []byte) error) error {
|
||||
// RFC 5280, 4.2.1.6
|
||||
|
||||
// SubjectAltName ::= GeneralNames
|
||||
//
|
||||
// GeneralNames ::= SEQUENCE SIZE (1..MAX) OF GeneralName
|
||||
//
|
||||
// GeneralName ::= CHOICE {
|
||||
// otherName [0] OtherName,
|
||||
// rfc822Name [1] IA5String,
|
||||
// dNSName [2] IA5String,
|
||||
// x400Address [3] ORAddress,
|
||||
// directoryName [4] Name,
|
||||
// ediPartyName [5] EDIPartyName,
|
||||
// uniformResourceIdentifier [6] IA5String,
|
||||
// iPAddress [7] OCTET STRING,
|
||||
// registeredID [8] OBJECT IDENTIFIER }
|
||||
var seq asn1.RawValue
|
||||
rest, err := asn1.Unmarshal(extension, &seq)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if len(rest) != 0 {
|
||||
return errors.New("x509: trailing data after X.509 extension")
|
||||
}
|
||||
if !seq.IsCompound || seq.Tag != 16 || seq.Class != 0 {
|
||||
return asn1.StructuralError{Msg: "bad SAN sequence"}
|
||||
}
|
||||
|
||||
rest = seq.Bytes
|
||||
for len(rest) > 0 {
|
||||
var v asn1.RawValue
|
||||
rest, err = asn1.Unmarshal(rest, &v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := callback(v.Tag, v.Bytes); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
const (
|
||||
nameTypeDN = 4
|
||||
)
|
||||
|
||||
var (
|
||||
tcgKpAIKCertificate = asn1.ObjectIdentifier{2, 23, 133, 8, 3}
|
||||
tcgAtTpmManufacturer = asn1.ObjectIdentifier{2, 23, 133, 2, 1}
|
||||
tcgAtTpmModel = asn1.ObjectIdentifier{2, 23, 133, 2, 2}
|
||||
tcgAtTpmVersion = asn1.ObjectIdentifier{2, 23, 133, 2, 3}
|
||||
)
|
||||
|
||||
func parseSANExtension(value []byte) (manufacturer string, model string, version string, err error) {
|
||||
err = forEachSAN(value, func(tag int, data []byte) error {
|
||||
switch tag {
|
||||
case nameTypeDN:
|
||||
tpmDeviceAttributes := pkix.RDNSequence{}
|
||||
_, err := asn1.Unmarshal(data, &tpmDeviceAttributes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, rdn := range tpmDeviceAttributes {
|
||||
if len(rdn) == 0 {
|
||||
continue
|
||||
}
|
||||
for _, atv := range rdn {
|
||||
value, ok := atv.Value.(string)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if atv.Type.Equal(tcgAtTpmManufacturer) {
|
||||
manufacturer = strings.TrimPrefix(value, "id:")
|
||||
}
|
||||
if atv.Type.Equal(tcgAtTpmModel) {
|
||||
model = value
|
||||
}
|
||||
if atv.Type.Equal(tcgAtTpmVersion) {
|
||||
version = strings.TrimPrefix(value, "id:")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var tpmManufacturers = []struct {
|
||||
id string
|
||||
name string
|
||||
code string
|
||||
}{
|
||||
{"414D4400", "AMD", "AMD"},
|
||||
{"41544D4C", "Atmel", "ATML"},
|
||||
{"4252434D", "Broadcom", "BRCM"},
|
||||
{"49424d00", "IBM", "IBM"},
|
||||
{"49465800", "Infineon", "IFX"},
|
||||
{"494E5443", "Intel", "INTC"},
|
||||
{"4C454E00", "Lenovo", "LEN"},
|
||||
{"4E534D20", "National Semiconductor", "NSM"},
|
||||
{"4E545A00", "Nationz", "NTZ"},
|
||||
{"4E544300", "Nuvoton Technology", "NTC"},
|
||||
{"51434F4D", "Qualcomm", "QCOM"},
|
||||
{"534D5343", "SMSC", "SMSC"},
|
||||
{"53544D20", "ST Microelectronics", "STM"},
|
||||
{"534D534E", "Samsung", "SMSN"},
|
||||
{"534E5300", "Sinosun", "SNS"},
|
||||
{"54584E00", "Texas Instruments", "TXN"},
|
||||
{"57454300", "Winbond", "WEC"},
|
||||
{"524F4343", "Fuzhouk Rockchip", "ROCC"},
|
||||
{"FFFFF1D0", "FIDO Alliance Conformance Testing", "FIDO"},
|
||||
}
|
||||
|
||||
func isValidTPMManufacturer(id string) bool {
|
||||
for _, m := range tpmManufacturers {
|
||||
if m.id == id {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/x509"
|
||||
|
||||
"github.com/duo-labs/webauthn/protocol/webauthncose"
|
||||
"github.com/fxamacker/cbor/v2"
|
||||
)
|
||||
|
||||
var u2fAttestationKey = "fido-u2f"
|
||||
|
||||
func init() {
|
||||
RegisterAttestationFormat(u2fAttestationKey, verifyU2FFormat)
|
||||
}
|
||||
|
||||
// verifyU2FFormat - Follows verification steps set out by https://www.w3.org/TR/webauthn/#fido-u2f-attestation
|
||||
func verifyU2FFormat(att AttestationObject, clientDataHash []byte) (string, []interface{}, error) {
|
||||
|
||||
if !bytes.Equal(att.AuthData.AttData.AAGUID, []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}) {
|
||||
return u2fAttestationKey, nil, ErrUnsupportedAlgorithm.WithDetails("U2F attestation format AAGUID not set to 0x00")
|
||||
}
|
||||
// Signing procedure step - If the credential public key of the given credential is not of
|
||||
// algorithm -7 ("ES256"), stop and return an error.
|
||||
key := webauthncose.EC2PublicKeyData{}
|
||||
cbor.Unmarshal(att.AuthData.AttData.CredentialPublicKey, &key)
|
||||
|
||||
if webauthncose.COSEAlgorithmIdentifier(key.PublicKeyData.Algorithm) != webauthncose.AlgES256 {
|
||||
return u2fAttestationKey, nil, ErrUnsupportedAlgorithm.WithDetails("Non-ES256 Public Key algorithm used")
|
||||
}
|
||||
|
||||
// U2F Step 1. Verify that attStmt is valid CBOR conforming to the syntax defined above
|
||||
// and perform CBOR decoding on it to extract the contained fields.
|
||||
|
||||
// The Format/syntax is
|
||||
// u2fStmtFormat = {
|
||||
// x5c: [ attestnCert: bytes ],
|
||||
// sig: bytes
|
||||
// }
|
||||
|
||||
// Check for "x5c" which is a single element array containing the attestation certificate in X.509 format.
|
||||
x5c, present := att.AttStatement["x5c"].([]interface{})
|
||||
if !present {
|
||||
return u2fAttestationKey, nil, ErrAttestationFormat.WithDetails("Missing properly formatted x5c data")
|
||||
}
|
||||
|
||||
// Check for "sig" which is The attestation signature. The signature was calculated over the (raw) U2F
|
||||
// registration response message https://www.w3.org/TR/webauthn/#biblio-fido-u2f-message-formats]
|
||||
// received by the client from the authenticator.
|
||||
signature, present := att.AttStatement["sig"].([]byte)
|
||||
if !present {
|
||||
return u2fAttestationKey, nil, ErrAttestationFormat.WithDetails("Missing sig data")
|
||||
}
|
||||
|
||||
// U2F Step 2. (1) Check that x5c has exactly one element and let attCert be that element. (2) Let certificate public
|
||||
// key be the public key conveyed by attCert. (3) If certificate public key is not an Elliptic Curve (EC) public
|
||||
// key over the P-256 curve, terminate this algorithm and return an appropriate error.
|
||||
|
||||
// Step 2.1
|
||||
if len(x5c) > 1 {
|
||||
return u2fAttestationKey, nil, ErrAttestationFormat.WithDetails("Received more than one element in x5c values")
|
||||
}
|
||||
|
||||
// Note: Packed Attestation, FIDO U2F Attestation, and Assertion Signatures support ASN.1,but it is recommended
|
||||
// that any new attestation formats defined not use ASN.1 encodings, but instead represent signatures as equivalent
|
||||
// fixed-length byte arrays without internal structure, using the same representations as used by COSE signatures
|
||||
// as defined in RFC8152 (https://www.w3.org/TR/webauthn/#biblio-rfc8152)
|
||||
// and RFC8230 (https://www.w3.org/TR/webauthn/#biblio-rfc8230).
|
||||
|
||||
// Step 2.2
|
||||
asn1Bytes, decoded := x5c[0].([]byte)
|
||||
if !decoded {
|
||||
return u2fAttestationKey, nil, ErrAttestationFormat.WithDetails("Error decoding ASN.1 data from x5c")
|
||||
}
|
||||
|
||||
attCert, err := x509.ParseCertificate(asn1Bytes)
|
||||
if err != nil {
|
||||
return u2fAttestationKey, nil, ErrAttestationFormat.WithDetails("Error parsing certificate from ASN.1 data into certificate")
|
||||
}
|
||||
|
||||
// Step 2.3
|
||||
if attCert.PublicKeyAlgorithm != x509.ECDSA && attCert.PublicKey.(*ecdsa.PublicKey).Curve != elliptic.P256() {
|
||||
return u2fAttestationKey, nil, ErrAttestationFormat.WithDetails("Attestation certificate is in invalid format")
|
||||
}
|
||||
|
||||
// Step 3. Extract the claimed rpIdHash from authenticatorData, and the claimed credentialId and credentialPublicKey
|
||||
// from authenticatorData.attestedCredentialData.
|
||||
|
||||
rpIdHash := att.AuthData.RPIDHash
|
||||
|
||||
credentialID := att.AuthData.AttData.CredentialID
|
||||
|
||||
// credentialPublicKey handled earlier
|
||||
|
||||
// Step 4. Convert the COSE_KEY formatted credentialPublicKey (see Section 7 of RFC8152 [https://www.w3.org/TR/webauthn/#biblio-rfc8152])
|
||||
// to Raw ANSI X9.62 public key format (see ALG_KEY_ECC_X962_RAW in Section 3.6.2 Public Key
|
||||
// Representation Formats of FIDO-Registry [https://www.w3.org/TR/webauthn/#biblio-fido-registry]).
|
||||
|
||||
// Let x be the value corresponding to the "-2" key (representing x coordinate) in credentialPublicKey, and confirm
|
||||
// its size to be of 32 bytes. If size differs or "-2" key is not found, terminate this algorithm and
|
||||
// return an appropriate error.
|
||||
|
||||
// Let y be the value corresponding to the "-3" key (representing y coordinate) in credentialPublicKey, and confirm
|
||||
// its size to be of 32 bytes. If size differs or "-3" key is not found, terminate this algorithm and
|
||||
// return an appropriate error.
|
||||
|
||||
if len(key.XCoord) > 32 || len(key.YCoord) > 32 {
|
||||
return u2fAttestationKey, nil, ErrAttestation.WithDetails("X or Y Coordinate for key is invalid length")
|
||||
}
|
||||
|
||||
// Let publicKeyU2F be the concatenation 0x04 || x || y.
|
||||
publicKeyU2F := bytes.NewBuffer([]byte{0x04})
|
||||
publicKeyU2F.Write(key.XCoord)
|
||||
publicKeyU2F.Write(key.YCoord)
|
||||
|
||||
// Step 5. Let verificationData be the concatenation of (0x00 || rpIdHash || clientDataHash || credentialId || publicKeyU2F)
|
||||
// (see §4.3 of FIDO-U2F-Message-Formats [https://www.w3.org/TR/webauthn/#biblio-fido-u2f-message-formats]).
|
||||
|
||||
verificationData := bytes.NewBuffer([]byte{0x00})
|
||||
verificationData.Write(rpIdHash)
|
||||
verificationData.Write(clientDataHash)
|
||||
verificationData.Write(credentialID)
|
||||
verificationData.Write(publicKeyU2F.Bytes())
|
||||
|
||||
// Step 6. Verify the sig using verificationData and certificate public key per SEC1[https://www.w3.org/TR/webauthn/#biblio-sec1].
|
||||
sigErr := attCert.CheckSignature(x509.ECDSAWithSHA256, verificationData.Bytes(), signature)
|
||||
if sigErr != nil {
|
||||
return u2fAttestationKey, nil, sigErr
|
||||
}
|
||||
|
||||
// Step 7. If successful, return attestation type Basic with the attestation trust path set to x5c.
|
||||
return "Fido U2F Basic", x5c, sigErr
|
||||
}
|
||||
+256
@@ -0,0 +1,256 @@
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
|
||||
"github.com/fxamacker/cbor/v2"
|
||||
)
|
||||
|
||||
var minAuthDataLength = 37
|
||||
|
||||
// Authenticators respond to Relying Party requests by returning an object derived from the
|
||||
// AuthenticatorResponse interface. See §5.2. Authenticator Responses
|
||||
// https://www.w3.org/TR/webauthn/#iface-authenticatorresponse
|
||||
type AuthenticatorResponse struct {
|
||||
// From the spec https://www.w3.org/TR/webauthn/#dom-authenticatorresponse-clientdatajson
|
||||
// This attribute contains a JSON serialization of the client data passed to the authenticator
|
||||
// by the client in its call to either create() or get().
|
||||
ClientDataJSON URLEncodedBase64 `json:"clientDataJSON"`
|
||||
}
|
||||
|
||||
// AuthenticatorData From §6.1 of the spec.
|
||||
// The authenticator data structure encodes contextual bindings made by the authenticator. These bindings
|
||||
// are controlled by the authenticator itself, and derive their trust from the WebAuthn Relying Party's
|
||||
// assessment of the security properties of the authenticator. In one extreme case, the authenticator
|
||||
// may be embedded in the client, and its bindings may be no more trustworthy than the client data.
|
||||
// At the other extreme, the authenticator may be a discrete entity with high-security hardware and
|
||||
// software, connected to the client over a secure channel. In both cases, the Relying Party receives
|
||||
// the authenticator data in the same format, and uses its knowledge of the authenticator to make
|
||||
// trust decisions.
|
||||
//
|
||||
// The authenticator data, at least during attestation, contains the Public Key that the RP stores
|
||||
// and will associate with the user attempting to register.
|
||||
type AuthenticatorData struct {
|
||||
RPIDHash []byte `json:"rpid"`
|
||||
Flags AuthenticatorFlags `json:"flags"`
|
||||
Counter uint32 `json:"sign_count"`
|
||||
AttData AttestedCredentialData `json:"att_data"`
|
||||
ExtData []byte `json:"ext_data"`
|
||||
}
|
||||
|
||||
type AttestedCredentialData struct {
|
||||
AAGUID []byte `json:"aaguid"`
|
||||
CredentialID []byte `json:"credential_id"`
|
||||
// The raw credential public key bytes received from the attestation data
|
||||
CredentialPublicKey []byte `json:"public_key"`
|
||||
}
|
||||
|
||||
// AuthenticatorAttachment https://www.w3.org/TR/webauthn/#platform-attachment
|
||||
type AuthenticatorAttachment string
|
||||
|
||||
const (
|
||||
// Platform - A platform authenticator is attached using a client device-specific transport, called
|
||||
// platform attachment, and is usually not removable from the client device. A public key credential
|
||||
// bound to a platform authenticator is called a platform credential.
|
||||
Platform AuthenticatorAttachment = "platform"
|
||||
// CrossPlatform A roaming authenticator is attached using cross-platform transports, called
|
||||
// cross-platform attachment. Authenticators of this class are removable from, and can "roam"
|
||||
// among, client devices. A public key credential bound to a roaming authenticator is called a
|
||||
// roaming credential.
|
||||
CrossPlatform AuthenticatorAttachment = "cross-platform"
|
||||
)
|
||||
|
||||
// Authenticators may implement various transports for communicating with clients. This enumeration defines
|
||||
// hints as to how clients might communicate with a particular authenticator in order to obtain an assertion
|
||||
// for a specific credential. Note that these hints represent the WebAuthn Relying Party's best belief as to
|
||||
// how an authenticator may be reached. A Relying Party may obtain a list of transports hints from some
|
||||
// attestation statement formats or via some out-of-band mechanism; it is outside the scope of this
|
||||
// specification to define that mechanism.
|
||||
// See §5.10.4. Authenticator Transport https://www.w3.org/TR/webauthn/#transport
|
||||
type AuthenticatorTransport string
|
||||
|
||||
const (
|
||||
// USB The authenticator should transport information over USB
|
||||
USB AuthenticatorTransport = "usb"
|
||||
// NFC The authenticator should transport information over Near Field Communication Protocol
|
||||
NFC AuthenticatorTransport = "nfc"
|
||||
// BLE The authenticator should transport information over Bluetooth
|
||||
BLE AuthenticatorTransport = "ble"
|
||||
// Internal the client should use an internal source like a TPM or SE
|
||||
Internal AuthenticatorTransport = "internal"
|
||||
)
|
||||
|
||||
// A WebAuthn Relying Party may require user verification for some of its operations but not for others,
|
||||
// and may use this type to express its needs.
|
||||
// See §5.10.6. User Verification Requirement Enumeration https://www.w3.org/TR/webauthn/#userVerificationRequirement
|
||||
type UserVerificationRequirement string
|
||||
|
||||
const (
|
||||
// VerificationRequired User verification is required to create/release a credential
|
||||
VerificationRequired UserVerificationRequirement = "required"
|
||||
// VerificationPreferred User verification is preferred to create/release a credential
|
||||
VerificationPreferred UserVerificationRequirement = "preferred" // This is the default
|
||||
// VerificationDiscouraged The authenticator should not verify the user for the credential
|
||||
VerificationDiscouraged UserVerificationRequirement = "discouraged"
|
||||
)
|
||||
|
||||
// AuthenticatorFlags A byte of information returned during during ceremonies in the
|
||||
// authenticatorData that contains bits that give us information about the
|
||||
// whether the user was present and/or verified during authentication, and whether
|
||||
// there is attestation or extension data present. Bit 0 is the least significant bit.
|
||||
type AuthenticatorFlags byte
|
||||
|
||||
// The bits that do not have flags are reserved for future use.
|
||||
const (
|
||||
// FlagUserPresent Bit 00000001 in the byte sequence. Tells us if user is present
|
||||
FlagUserPresent AuthenticatorFlags = 1 << iota // Referred to as UP
|
||||
_ // Reserved
|
||||
// FlagUserVerified Bit 00000100 in the byte sequence. Tells us if user is verified
|
||||
// by the authenticator using a biometric or PIN
|
||||
FlagUserVerified // Referred to as UV
|
||||
_ // Reserved
|
||||
_ // Reserved
|
||||
_ // Reserved
|
||||
// FlagAttestedCredentialData Bit 01000000 in the byte sequence. Indicates whether
|
||||
// the authenticator added attested credential data.
|
||||
FlagAttestedCredentialData // Referred to as AT
|
||||
// FlagHasExtension Bit 10000000 in the byte sequence. Indicates if the authenticator data has extensions.
|
||||
FlagHasExtensions // Referred to as ED
|
||||
)
|
||||
|
||||
// UserPresent returns if the UP flag was set
|
||||
func (flag AuthenticatorFlags) UserPresent() bool {
|
||||
return (flag & FlagUserPresent) == FlagUserPresent
|
||||
}
|
||||
|
||||
// UserVerified returns if the UV flag was set
|
||||
func (flag AuthenticatorFlags) UserVerified() bool {
|
||||
return (flag & FlagUserVerified) == FlagUserVerified
|
||||
}
|
||||
|
||||
// HasAttestedCredentialData returns if the AT flag was set
|
||||
func (flag AuthenticatorFlags) HasAttestedCredentialData() bool {
|
||||
return (flag & FlagAttestedCredentialData) == FlagAttestedCredentialData
|
||||
}
|
||||
|
||||
// HasExtensions returns if the ED flag was set
|
||||
func (flag AuthenticatorFlags) HasExtensions() bool {
|
||||
return (flag & FlagHasExtensions) == FlagHasExtensions
|
||||
}
|
||||
|
||||
// Unmarshal will take the raw Authenticator Data and marshalls it into AuthenticatorData for further validation.
|
||||
// The authenticator data has a compact but extensible encoding. This is desired since authenticators can be
|
||||
// devices with limited capabilities and low power requirements, with much simpler software stacks than the client platform.
|
||||
// The authenticator data structure is a byte array of 37 bytes or more, and is laid out in this table:
|
||||
// https://www.w3.org/TR/webauthn/#table-authData
|
||||
func (a *AuthenticatorData) Unmarshal(rawAuthData []byte) error {
|
||||
if minAuthDataLength > len(rawAuthData) {
|
||||
err := ErrBadRequest.WithDetails("Authenticator data length too short")
|
||||
info := fmt.Sprintf("Expected data greater than %d bytes. Got %d bytes\n", minAuthDataLength, len(rawAuthData))
|
||||
return err.WithInfo(info)
|
||||
}
|
||||
|
||||
a.RPIDHash = rawAuthData[:32]
|
||||
a.Flags = AuthenticatorFlags(rawAuthData[32])
|
||||
a.Counter = binary.BigEndian.Uint32(rawAuthData[33:37])
|
||||
|
||||
remaining := len(rawAuthData) - minAuthDataLength
|
||||
|
||||
if a.Flags.HasAttestedCredentialData() {
|
||||
if len(rawAuthData) > minAuthDataLength {
|
||||
a.unmarshalAttestedData(rawAuthData)
|
||||
attDataLen := len(a.AttData.AAGUID) + 2 + len(a.AttData.CredentialID) + len(a.AttData.CredentialPublicKey)
|
||||
remaining = remaining - attDataLen
|
||||
} else {
|
||||
return ErrBadRequest.WithDetails("Attested credential flag set but data is missing")
|
||||
}
|
||||
} else {
|
||||
if !a.Flags.HasExtensions() && len(rawAuthData) != 37 {
|
||||
return ErrBadRequest.WithDetails("Attested credential flag not set")
|
||||
}
|
||||
}
|
||||
|
||||
if a.Flags.HasExtensions() {
|
||||
if remaining != 0 {
|
||||
a.ExtData = rawAuthData[len(rawAuthData)-remaining:]
|
||||
remaining -= len(a.ExtData)
|
||||
} else {
|
||||
return ErrBadRequest.WithDetails("Extensions flag set but extensions data is missing")
|
||||
}
|
||||
}
|
||||
|
||||
if remaining != 0 {
|
||||
return ErrBadRequest.WithDetails("Leftover bytes decoding AuthenticatorData")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// If Attestation Data is present, unmarshall that into the appropriate public key structure
|
||||
func (a *AuthenticatorData) unmarshalAttestedData(rawAuthData []byte) {
|
||||
a.AttData.AAGUID = rawAuthData[37:53]
|
||||
idLength := binary.BigEndian.Uint16(rawAuthData[53:55])
|
||||
a.AttData.CredentialID = rawAuthData[55 : 55+idLength]
|
||||
a.AttData.CredentialPublicKey = unmarshalCredentialPublicKey(rawAuthData[55+idLength:])
|
||||
}
|
||||
|
||||
// Unmarshall the credential's Public Key into CBOR encoding
|
||||
func unmarshalCredentialPublicKey(keyBytes []byte) []byte {
|
||||
var m interface{}
|
||||
cbor.Unmarshal(keyBytes, &m)
|
||||
rawBytes, _ := cbor.Marshal(m)
|
||||
return rawBytes
|
||||
}
|
||||
|
||||
// ResidentKeyRequired - Require that the key be private key resident to the client device
|
||||
func ResidentKeyRequired() *bool {
|
||||
required := true
|
||||
return &required
|
||||
}
|
||||
|
||||
// ResidentKeyUnrequired - Do not require that the private key be resident to the client device.
|
||||
func ResidentKeyUnrequired() *bool {
|
||||
required := false
|
||||
return &required
|
||||
}
|
||||
|
||||
// Verify on AuthenticatorData handles Steps 9 through 12 for Registration
|
||||
// and Steps 11 through 14 for Assertion.
|
||||
func (a *AuthenticatorData) Verify(rpIdHash, appIDHash []byte, userVerificationRequired bool) error {
|
||||
|
||||
// Registration Step 9 & Assertion Step 11
|
||||
// Verify that the RP ID hash in authData is indeed the SHA-256
|
||||
// hash of the RP ID expected by the RP.
|
||||
if !bytes.Equal(a.RPIDHash[:], rpIdHash) && !bytes.Equal(a.RPIDHash[:], appIDHash) {
|
||||
return ErrVerification.WithInfo(fmt.Sprintf("RP Hash mismatch. Expected %s and Received %s\n", a.RPIDHash, rpIdHash))
|
||||
}
|
||||
|
||||
// Registration Step 10 & Assertion Step 12
|
||||
// Verify that the User Present bit of the flags in authData is set.
|
||||
if !a.Flags.UserPresent() {
|
||||
return ErrVerification.WithInfo(fmt.Sprintln("User presence flag not set by authenticator"))
|
||||
}
|
||||
|
||||
// Registration Step 11 & Assertion Step 13
|
||||
// If user verification is required for this assertion, verify that
|
||||
// the User Verified bit of the flags in authData is set.
|
||||
if userVerificationRequired && !a.Flags.UserVerified() {
|
||||
return ErrVerification.WithInfo(fmt.Sprintln("User verification required but flag not set by authenticator"))
|
||||
}
|
||||
|
||||
// Registration Step 12 & Assertion Step 14
|
||||
// Verify that the values of the client extension outputs in clientExtensionResults
|
||||
// and the authenticator extension outputs in the extensions in authData are as
|
||||
// expected, considering the client extension input values that were given as the
|
||||
// extensions option in the create() call. In particular, any extension identifier
|
||||
// values in the clientExtensionResults and the extensions in authData MUST be also be
|
||||
// present as extension identifier values in the extensions member of options, i.e., no
|
||||
// extensions are present that were not requested. In the general case, the meaning
|
||||
// of "are as expected" is specific to the Relying Party and which extensions are in use.
|
||||
|
||||
// This is not yet fully implemented by the spec or by browsers
|
||||
|
||||
return nil
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// URLEncodedBase64 represents a byte slice holding URL-encoded base64 data.
|
||||
// When fields of this type are unmarshaled from JSON, the data is base64
|
||||
// decoded into a byte slice.
|
||||
type URLEncodedBase64 []byte
|
||||
|
||||
// UnmarshalJSON base64 decodes a URL-encoded value, storing the result in the
|
||||
// provided byte slice.
|
||||
func (dest *URLEncodedBase64) UnmarshalJSON(data []byte) error {
|
||||
if bytes.Equal(data, []byte("null")) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Trim the leading spaces
|
||||
data = bytes.Trim(data, "\"")
|
||||
out := make([]byte, base64.RawURLEncoding.DecodedLen(len(data)))
|
||||
n, err := base64.RawURLEncoding.Decode(out, data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
v := reflect.ValueOf(dest).Elem()
|
||||
v.SetBytes(out[:n])
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalJSON base64 encodes a non URL-encoded value, storing the result in the
|
||||
// provided byte slice.
|
||||
func (data URLEncodedBase64) MarshalJSON() ([]byte, error) {
|
||||
if data == nil {
|
||||
return []byte("null"), nil
|
||||
}
|
||||
return []byte(`"` + base64.RawURLEncoding.EncodeToString(data) + `"`), nil
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
)
|
||||
|
||||
// ChallengeLength - Length of bytes to generate for a challenge
|
||||
const ChallengeLength = 32
|
||||
|
||||
// Challenge that should be signed and returned by the authenticator
|
||||
type Challenge URLEncodedBase64
|
||||
|
||||
// Create a new challenge to be sent to the authenticator. The spec recommends using
|
||||
// at least 16 bytes with 100 bits of entropy. We use 32 bytes.
|
||||
func CreateChallenge() (Challenge, error) {
|
||||
challenge := make([]byte, ChallengeLength)
|
||||
_, err := rand.Read(challenge)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return challenge, nil
|
||||
}
|
||||
|
||||
func (c Challenge) String() string {
|
||||
return base64.RawURLEncoding.EncodeToString(c)
|
||||
}
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// CollectedClientData represents the contextual bindings of both the WebAuthn Relying Party
|
||||
// and the client. It is a key-value mapping whose keys are strings. Values can be any type
|
||||
// that has a valid encoding in JSON. Its structure is defined by the following Web IDL.
|
||||
// https://www.w3.org/TR/webauthn/#sec-client-data
|
||||
type CollectedClientData struct {
|
||||
// Type the string "webauthn.create" when creating new credentials,
|
||||
// and "webauthn.get" when getting an assertion from an existing credential. The
|
||||
// purpose of this member is to prevent certain types of signature confusion attacks
|
||||
//(where an attacker substitutes one legitimate signature for another).
|
||||
Type CeremonyType `json:"type"`
|
||||
Challenge string `json:"challenge"`
|
||||
Origin string `json:"origin"`
|
||||
TokenBinding *TokenBinding `json:"tokenBinding,omitempty"`
|
||||
// Chromium (Chrome) returns a hint sometimes about how to handle clientDataJSON in a safe manner
|
||||
Hint string `json:"new_keys_may_be_added_here,omitempty"`
|
||||
}
|
||||
|
||||
type CeremonyType string
|
||||
|
||||
const (
|
||||
CreateCeremony CeremonyType = "webauthn.create"
|
||||
AssertCeremony CeremonyType = "webauthn.get"
|
||||
)
|
||||
|
||||
type TokenBinding struct {
|
||||
Status TokenBindingStatus `json:"status"`
|
||||
ID string `json:"id,omitempty"`
|
||||
}
|
||||
|
||||
type TokenBindingStatus string
|
||||
|
||||
const (
|
||||
// Indicates token binding was used when communicating with the
|
||||
// Relying Party. In this case, the id member MUST be present.
|
||||
Present TokenBindingStatus = "present"
|
||||
// Indicates token binding was used when communicating with the
|
||||
// negotiated when communicating with the Relying Party.
|
||||
Supported TokenBindingStatus = "supported"
|
||||
// Indicates token binding not supported
|
||||
// when communicating with the Relying Party.
|
||||
NotSupported TokenBindingStatus = "not-supported"
|
||||
)
|
||||
|
||||
// Returns the origin per the HTML spec: (scheme)://(host)[:(port)]
|
||||
func FullyQualifiedOrigin(u *url.URL) string {
|
||||
return fmt.Sprintf("%s://%s", u.Scheme, u.Host)
|
||||
}
|
||||
|
||||
// Handles steps 3 through 6 of verfying the registering client data of a
|
||||
// new credential and steps 7 through 10 of verifying an authentication assertion
|
||||
// See https://www.w3.org/TR/webauthn/#registering-a-new-credential
|
||||
// and https://www.w3.org/TR/webauthn/#verifying-assertion
|
||||
func (c *CollectedClientData) Verify(storedChallenge string, ceremony CeremonyType, relyingPartyOrigin string) error {
|
||||
|
||||
// Registration Step 3. Verify that the value of C.type is webauthn.create.
|
||||
|
||||
// Assertion Step 7. Verify that the value of C.type is the string webauthn.get.
|
||||
if c.Type != ceremony {
|
||||
err := ErrVerification.WithDetails("Error validating ceremony type")
|
||||
err.WithInfo(fmt.Sprintf("Expected Value: %s\n Received: %s\n", ceremony, c.Type))
|
||||
return err
|
||||
}
|
||||
|
||||
// Registration Step 4. Verify that the value of C.challenge matches the challenge
|
||||
// that was sent to the authenticator in the create() call.
|
||||
|
||||
// Assertion Step 8. Verify that the value of C.challenge matches the challenge
|
||||
// that was sent to the authenticator in the PublicKeyCredentialRequestOptions
|
||||
// passed to the get() call.
|
||||
|
||||
challenge := c.Challenge
|
||||
if 0 != strings.Compare(storedChallenge, challenge) {
|
||||
err := ErrVerification.WithDetails("Error validating challenge")
|
||||
return err.WithInfo(fmt.Sprintf("Expected b Value: %#v\nReceived b: %#v\n", storedChallenge, challenge))
|
||||
}
|
||||
|
||||
// Registration Step 5 & Assertion Step 9. Verify that the value of C.origin matches
|
||||
// the Relying Party's origin.
|
||||
clientDataOrigin, err := url.Parse(c.Origin)
|
||||
if err != nil {
|
||||
return ErrParsingData.WithDetails("Error decoding clientData origin as URL")
|
||||
}
|
||||
|
||||
if !strings.EqualFold(FullyQualifiedOrigin(clientDataOrigin), relyingPartyOrigin) {
|
||||
err := ErrVerification.WithDetails("Error validating origin")
|
||||
return err.WithInfo(fmt.Sprintf("Expected Value: %s\n Received: %s\n", relyingPartyOrigin, FullyQualifiedOrigin(clientDataOrigin)))
|
||||
}
|
||||
|
||||
// Registration Step 6 and Assertion Step 10. Verify that the value of C.tokenBinding.status
|
||||
// matches the state of Token Binding for the TLS connection over which the assertion was
|
||||
// obtained. If Token Binding was used on that TLS connection, also verify that C.tokenBinding.id
|
||||
// matches the base64url encoding of the Token Binding ID for the connection.
|
||||
if c.TokenBinding != nil {
|
||||
if c.TokenBinding.Status == "" {
|
||||
return ErrParsingData.WithDetails("Error decoding clientData, token binding present without status")
|
||||
}
|
||||
if c.TokenBinding.Status != Present && c.TokenBinding.Status != Supported && c.TokenBinding.Status != NotSupported {
|
||||
return ErrParsingData.WithDetails("Error decoding clientData, token binding present with invalid status").WithInfo(fmt.Sprintf("Got: %s\n", c.TokenBinding.Status))
|
||||
}
|
||||
}
|
||||
// Not yet fully implemented by the spec, browsers, and me.
|
||||
|
||||
return nil
|
||||
}
|
||||
+216
@@ -0,0 +1,216 @@
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// The basic credential type that is inherited by WebAuthn's
|
||||
// PublicKeyCredential type
|
||||
// https://w3c.github.io/webappsec-credential-management/#credential
|
||||
type Credential struct {
|
||||
// ID is The credential’s identifier. The requirements for the
|
||||
// identifier are distinct for each type of credential. It might
|
||||
// represent a username for username/password tuples, for example.
|
||||
ID string `json:"id"`
|
||||
// Type is the value of the object’s interface object's [[type]] slot,
|
||||
// which specifies the credential type represented by this object.
|
||||
// This should be type "public-key" for Webauthn credentials.
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
// The PublicKeyCredential interface inherits from Credential, and contains
|
||||
// the attributes that are returned to the caller when a new credential
|
||||
// is created, or a new assertion is requested.
|
||||
type ParsedCredential struct {
|
||||
ID string `cbor:"id"`
|
||||
Type string `cbor:"type"`
|
||||
}
|
||||
|
||||
type PublicKeyCredential struct {
|
||||
Credential
|
||||
RawID URLEncodedBase64 `json:"rawId"`
|
||||
ClientExtensionResults AuthenticationExtensionsClientOutputs `json:"clientExtensionResults,omitempty"`
|
||||
}
|
||||
|
||||
type ParsedPublicKeyCredential struct {
|
||||
ParsedCredential
|
||||
RawID []byte `json:"rawId"`
|
||||
ClientExtensionResults AuthenticationExtensionsClientOutputs `json:"clientExtensionResults,omitempty"`
|
||||
}
|
||||
|
||||
type CredentialCreationResponse struct {
|
||||
PublicKeyCredential
|
||||
AttestationResponse AuthenticatorAttestationResponse `json:"response"`
|
||||
}
|
||||
|
||||
type ParsedCredentialCreationData struct {
|
||||
ParsedPublicKeyCredential
|
||||
Response ParsedAttestationResponse
|
||||
Raw CredentialCreationResponse
|
||||
}
|
||||
|
||||
func ParseCredentialCreationResponse(response *http.Request) (*ParsedCredentialCreationData, error) {
|
||||
if response == nil || response.Body == nil {
|
||||
return nil, ErrBadRequest.WithDetails("No response given")
|
||||
}
|
||||
return ParseCredentialCreationResponseBody(response.Body)
|
||||
}
|
||||
|
||||
func ParseCredentialCreationResponseBody(body io.Reader) (*ParsedCredentialCreationData, error) {
|
||||
var ccr CredentialCreationResponse
|
||||
err := json.NewDecoder(body).Decode(&ccr)
|
||||
if err != nil {
|
||||
return nil, ErrBadRequest.WithDetails("Parse error for Registration").WithInfo(err.Error())
|
||||
}
|
||||
|
||||
if ccr.ID == "" {
|
||||
return nil, ErrBadRequest.WithDetails("Parse error for Registration").WithInfo("Missing ID")
|
||||
}
|
||||
|
||||
testB64, err := base64.RawURLEncoding.DecodeString(ccr.ID)
|
||||
if err != nil || !(len(testB64) > 0) {
|
||||
return nil, ErrBadRequest.WithDetails("Parse error for Registration").WithInfo("ID not base64.RawURLEncoded")
|
||||
}
|
||||
|
||||
if ccr.PublicKeyCredential.Credential.Type == "" {
|
||||
return nil, ErrBadRequest.WithDetails("Parse error for Registration").WithInfo("Missing type")
|
||||
}
|
||||
|
||||
if ccr.PublicKeyCredential.Credential.Type != "public-key" {
|
||||
return nil, ErrBadRequest.WithDetails("Parse error for Registration").WithInfo("Type not public-key")
|
||||
}
|
||||
|
||||
var pcc ParsedCredentialCreationData
|
||||
pcc.ID, pcc.RawID, pcc.Type = ccr.ID, ccr.RawID, ccr.Type
|
||||
pcc.Raw = ccr
|
||||
|
||||
parsedAttestationResponse, err := ccr.AttestationResponse.Parse()
|
||||
if err != nil {
|
||||
return nil, ErrParsingData.WithDetails("Error parsing attestation response")
|
||||
}
|
||||
|
||||
pcc.Response = *parsedAttestationResponse
|
||||
|
||||
return &pcc, nil
|
||||
}
|
||||
|
||||
// Verifies the Client and Attestation data as laid out by §7.1. Registering a new credential
|
||||
// https://www.w3.org/TR/webauthn/#registering-a-new-credential
|
||||
func (pcc *ParsedCredentialCreationData) Verify(storedChallenge string, verifyUser bool, relyingPartyID, relyingPartyOrigin string) error {
|
||||
|
||||
// Handles steps 3 through 6 - Verifying the Client Data against the Relying Party's stored data
|
||||
verifyError := pcc.Response.CollectedClientData.Verify(storedChallenge, CreateCeremony, relyingPartyOrigin)
|
||||
if verifyError != nil {
|
||||
return verifyError
|
||||
}
|
||||
|
||||
// Step 7. Compute the hash of response.clientDataJSON using SHA-256.
|
||||
clientDataHash := sha256.Sum256(pcc.Raw.AttestationResponse.ClientDataJSON)
|
||||
|
||||
// Step 8. Perform CBOR decoding on the attestationObject field of the AuthenticatorAttestationResponse
|
||||
// structure to obtain the attestation statement format fmt, the authenticator data authData, and the
|
||||
// attestation statement attStmt. is handled while
|
||||
|
||||
// We do the above step while parsing and decoding the CredentialCreationResponse
|
||||
// Handle steps 9 through 14 - This verifies the attestaion object and
|
||||
verifyError = pcc.Response.AttestationObject.Verify(relyingPartyID, clientDataHash[:], verifyUser)
|
||||
if verifyError != nil {
|
||||
return verifyError
|
||||
}
|
||||
|
||||
// Step 15. If validation is successful, obtain a list of acceptable trust anchors (attestation root
|
||||
// certificates or ECDAA-Issuer public keys) for that attestation type and attestation statement
|
||||
// format fmt, from a trusted source or from policy. For example, the FIDO Metadata Service provides
|
||||
// one way to obtain such information, using the aaguid in the attestedCredentialData in authData.
|
||||
// [https://fidoalliance.org/specs/fido-v2.0-id-20180227/fido-metadata-service-v2.0-id-20180227.html]
|
||||
|
||||
// TODO: There are no valid AAGUIDs yet or trust sources supported. We could implement policy for the RP in
|
||||
// the future, however.
|
||||
|
||||
// Step 16. Assess the attestation trustworthiness using outputs of the verification procedure in step 14, as follows:
|
||||
// - If self attestation was used, check if self attestation is acceptable under Relying Party policy.
|
||||
// - If ECDAA was used, verify that the identifier of the ECDAA-Issuer public key used is included in
|
||||
// the set of acceptable trust anchors obtained in step 15.
|
||||
// - Otherwise, use the X.509 certificates returned by the verification procedure to verify that the
|
||||
// attestation public key correctly chains up to an acceptable root certificate.
|
||||
|
||||
// TODO: We're not supporting trust anchors, self-attestation policy, or acceptable root certs yet
|
||||
|
||||
// Step 17. Check that the credentialId is not yet registered to any other user. If registration is
|
||||
// requested for a credential that is already registered to a different user, the Relying Party SHOULD
|
||||
// fail this registration ceremony, or it MAY decide to accept the registration, e.g. while deleting
|
||||
// the older registration.
|
||||
|
||||
// TODO: We can't support this in the code's current form, the Relying Party would need to check for this
|
||||
// against their database
|
||||
|
||||
// Step 18 If the attestation statement attStmt verified successfully and is found to be trustworthy, then
|
||||
// register the new credential with the account that was denoted in the options.user passed to create(), by
|
||||
// associating it with the credentialId and credentialPublicKey in the attestedCredentialData in authData, as
|
||||
// appropriate for the Relying Party's system.
|
||||
|
||||
// Step 19. If the attestation statement attStmt successfully verified but is not trustworthy per step 16 above,
|
||||
// the Relying Party SHOULD fail the registration ceremony.
|
||||
|
||||
// TODO: Not implemented for the reasons mentioned under Step 16
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetAppID takes a AuthenticationExtensions object or nil. It then performs the following checks in order:
|
||||
//
|
||||
// 1. Check that the Session Data's AuthenticationExtensions has been provided and return a blank appid if it hasn't been.
|
||||
// 2. Check that the AuthenticationExtensionsClientOutputs contains the extensions output and return a blank appid if it doesn't.
|
||||
// 3. Check that the Credential AttestationType is `fido-u2f` and return a blank appid if it isn't.
|
||||
// 4. Check that the AuthenticationExtensionsClientOutputs contains the appid key and return a blank appid if it doesn't.
|
||||
// 5. Check that the AuthenticationExtensionsClientOutputs appid is a bool and return an error if it isn't.
|
||||
// 6. Check that the appid output is true and return a blank appid if it isn't.
|
||||
// 7. Check that the Session Data has an appid extension defined and return an error if it doesn't.
|
||||
// 8. Check that the appid extension in Session Data is a string and return an error if it isn't.
|
||||
// 9. Return the appid extension value from the Session Data.
|
||||
func (ppkc ParsedPublicKeyCredential) GetAppID(authExt AuthenticationExtensions, credentialAttestationType string) (appID string, err error) {
|
||||
var (
|
||||
value, clientValue interface{}
|
||||
enableAppID, ok bool
|
||||
)
|
||||
|
||||
if authExt == nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
if ppkc.ClientExtensionResults == nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// If the credential does not have the correct attestation type it is assumed to NOT be a fido-u2f credential.
|
||||
// https://w3c.github.io/webauthn/#sctn-fido-u2f-attestation
|
||||
if credentialAttestationType != "fido-u2f" {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
if clientValue, ok = ppkc.ClientExtensionResults["appid"]; !ok {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
if enableAppID, ok = clientValue.(bool); !ok {
|
||||
return "", ErrBadRequest.WithDetails("Client Output appid did not have the expected type")
|
||||
}
|
||||
|
||||
if !enableAppID {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
if value, ok = authExt["appid"]; !ok {
|
||||
return "", ErrBadRequest.WithDetails("Session Data does not have an appid but Client Output indicates it should be set")
|
||||
}
|
||||
|
||||
if appID, ok = value.(string); !ok {
|
||||
return "", ErrBadRequest.WithDetails("Session Data appid did not have the expected type")
|
||||
}
|
||||
|
||||
return appID, nil
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
// The protocol package contains data structures and validation functionality
|
||||
// outlined in the Web Authnentication specification (https://www.w3.org/TR/webauthn).
|
||||
// The data structures here attempt to conform as much as possible to their definitions,
|
||||
// but some structs (like those that are used as part of validation steps) contain
|
||||
// additional fields that help us unpack and validate the data we unmarshall.
|
||||
// When implementing this library, most developers will primarily be using the API
|
||||
// outlined in the webauthn package.
|
||||
package protocol
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
package protocol
|
||||
|
||||
// From §5.4.1 (https://www.w3.org/TR/webauthn/#dictionary-pkcredentialentity).
|
||||
// PublicKeyCredentialEntity describes a user account, or a WebAuthn Relying Party,
|
||||
// with which a public key credential is associated.
|
||||
type CredentialEntity struct {
|
||||
// A human-palatable name for the entity. Its function depends on what the PublicKeyCredentialEntity represents:
|
||||
//
|
||||
// When inherited by PublicKeyCredentialRpEntity it is a human-palatable identifier for the Relying Party,
|
||||
// intended only for display. For example, "ACME Corporation", "Wonderful Widgets, Inc." or "ОАО Примертех".
|
||||
//
|
||||
// When inherited by PublicKeyCredentialUserEntity, it is a human-palatable identifier for a user account. It is
|
||||
// intended only for display, i.e., aiding the user in determining the difference between user accounts with similar
|
||||
// displayNames. For example, "alexm", "alex.p.mueller@example.com" or "+14255551234".
|
||||
Name string `json:"name"`
|
||||
// A serialized URL which resolves to an image associated with the entity. For example,
|
||||
// this could be a user’s avatar or a Relying Party's logo. This URL MUST be an a priori
|
||||
// authenticated URL. Authenticators MUST accept and store a 128-byte minimum length for
|
||||
// an icon member’s value. Authenticators MAY ignore an icon member’s value if its length
|
||||
// is greater than 128 bytes. The URL’s scheme MAY be "data" to avoid fetches of the URL,
|
||||
// at the cost of needing more storage.
|
||||
Icon string `json:"icon,omitempty"`
|
||||
}
|
||||
|
||||
// From §5.4.2 (https://www.w3.org/TR/webauthn/#sctn-rp-credential-params).
|
||||
// The PublicKeyCredentialRpEntity is used to supply additional
|
||||
// Relying Party attributes when creating a new credential.
|
||||
type RelyingPartyEntity struct {
|
||||
CredentialEntity
|
||||
// A unique identifier for the Relying Party entity, which sets the RP ID.
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
// From §5.4.3 (https://www.w3.org/TR/webauthn/#sctn-user-credential-params).
|
||||
// The PublicKeyCredentialUserEntity is used to supply additional
|
||||
// user account attributes when creating a new credential.
|
||||
type UserEntity struct {
|
||||
CredentialEntity
|
||||
// A human-palatable name for the user account, intended only for display.
|
||||
// For example, "Alex P. Müller" or "田中 倫". The Relying Party SHOULD let
|
||||
// the user choose this, and SHOULD NOT restrict the choice more than necessary.
|
||||
DisplayName string `json:"displayName,omitempty"`
|
||||
// ID is the user handle of the user account entity. To ensure secure operation,
|
||||
// authentication and authorization decisions MUST be made on the basis of this id
|
||||
// member, not the displayName nor name members. See Section 6.1 of
|
||||
// [RFC8266](https://www.w3.org/TR/webauthn/#biblio-rfc8266).
|
||||
ID []byte `json:"id"`
|
||||
}
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
package protocol
|
||||
|
||||
type Error struct {
|
||||
// Short name for the type of error that has occurred
|
||||
Type string `json:"type"`
|
||||
// Additional details about the error
|
||||
Details string `json:"error"`
|
||||
// Information to help debug the error
|
||||
DevInfo string `json:"debug"`
|
||||
}
|
||||
|
||||
var (
|
||||
ErrBadRequest = &Error{
|
||||
Type: "invalid_request",
|
||||
Details: "Error reading the requst data",
|
||||
}
|
||||
ErrChallengeMismatch = &Error{
|
||||
Type: "challenge_mismatch",
|
||||
Details: "Stored challenge and received challenge do not match",
|
||||
}
|
||||
ErrParsingData = &Error{
|
||||
Type: "parse_error",
|
||||
Details: "Error parsing the authenticator response",
|
||||
}
|
||||
ErrAuthData = &Error{
|
||||
Type: "auth_data",
|
||||
Details: "Error verifying the authenticator data",
|
||||
}
|
||||
ErrVerification = &Error{
|
||||
Type: "verification_error",
|
||||
Details: "Error validating the authenticator response",
|
||||
}
|
||||
ErrAttestation = &Error{
|
||||
Type: "attesation_error",
|
||||
Details: "Error validating the attestation data provided",
|
||||
}
|
||||
ErrInvalidAttestation = &Error{
|
||||
Type: "invalid_attestation",
|
||||
Details: "Invalid attestation data",
|
||||
}
|
||||
ErrAttestationFormat = &Error{
|
||||
Type: "invalid_attestation",
|
||||
Details: "Invalid attestation format",
|
||||
}
|
||||
ErrAttestationCertificate = &Error{
|
||||
Type: "invalid_certificate",
|
||||
Details: "Invalid attestation certificate",
|
||||
}
|
||||
ErrAssertionSignature = &Error{
|
||||
Type: "invalid_signature",
|
||||
Details: "Assertion Signature against auth data and client hash is not valid",
|
||||
}
|
||||
ErrUnsupportedKey = &Error{
|
||||
Type: "invalid_key_type",
|
||||
Details: "Unsupported Public Key Type",
|
||||
}
|
||||
ErrUnsupportedAlgorithm = &Error{
|
||||
Type: "unsupported_key_algorithm",
|
||||
Details: "Unsupported public key algorithm",
|
||||
}
|
||||
ErrNotSpecImplemented = &Error{
|
||||
Type: "spec_unimplemented",
|
||||
Details: "This field is not yet supported by the WebAuthn spec",
|
||||
}
|
||||
ErrNotImplemented = &Error{
|
||||
Type: "not_implemented",
|
||||
Details: "This field is not yet supported by this library",
|
||||
}
|
||||
)
|
||||
|
||||
func (err *Error) Error() string {
|
||||
return err.Details
|
||||
}
|
||||
|
||||
func (passedError *Error) WithDetails(details string) *Error {
|
||||
err := *passedError
|
||||
err.Details = details
|
||||
return &err
|
||||
}
|
||||
|
||||
func (passedError *Error) WithInfo(info string) *Error {
|
||||
err := *passedError
|
||||
err.DevInfo = info
|
||||
return &err
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
package protocol
|
||||
|
||||
// Extensions are discussed in §9. WebAuthn Extensions (https://www.w3.org/TR/webauthn/#extensions).
|
||||
|
||||
// For a list of commonly supported extenstions, see §10. Defined Extensions
|
||||
// (https://www.w3.org/TR/webauthn/#sctn-defined-extensions).
|
||||
|
||||
type AuthenticationExtensionsClientOutputs map[string]interface{}
|
||||
+282
@@ -0,0 +1,282 @@
|
||||
package googletpm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha1"
|
||||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
"fmt"
|
||||
"hash"
|
||||
)
|
||||
|
||||
// DecodeAttestationData decode a TPMS_ATTEST message. No error is returned if
|
||||
// the input has extra trailing data.
|
||||
func DecodeAttestationData(in []byte) (*AttestationData, error) {
|
||||
buf := bytes.NewBuffer(in)
|
||||
|
||||
var ad AttestationData
|
||||
if err := UnpackBuf(buf, &ad.Magic, &ad.Type); err != nil {
|
||||
return nil, fmt.Errorf("decoding Magic/Type: %v", err)
|
||||
}
|
||||
n, err := decodeName(buf)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decoding QualifiedSigner: %v", err)
|
||||
}
|
||||
ad.QualifiedSigner = *n
|
||||
if err := UnpackBuf(buf, &ad.ExtraData, &ad.ClockInfo, &ad.FirmwareVersion); err != nil {
|
||||
return nil, fmt.Errorf("decoding ExtraData/ClockInfo/FirmwareVersion: %v", err)
|
||||
}
|
||||
|
||||
// The spec specifies several other types of attestation data. We only need
|
||||
// parsing of Certify & Creation attestation data for now. If you need
|
||||
// support for other attestation types, add them here.
|
||||
switch ad.Type {
|
||||
case TagAttestCertify:
|
||||
if ad.AttestedCertifyInfo, err = decodeCertifyInfo(buf); err != nil {
|
||||
return nil, fmt.Errorf("decoding AttestedCertifyInfo: %v", err)
|
||||
}
|
||||
case TagAttestCreation:
|
||||
if ad.AttestedCreationInfo, err = decodeCreationInfo(buf); err != nil {
|
||||
return nil, fmt.Errorf("decoding AttestedCreationInfo: %v", err)
|
||||
}
|
||||
case TagAttestQuote:
|
||||
if ad.AttestedQuoteInfo, err = decodeQuoteInfo(buf); err != nil {
|
||||
return nil, fmt.Errorf("decoding AttestedQuoteInfo: %v", err)
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("only Certify & Creation attestation structures are supported, got type 0x%x", ad.Type)
|
||||
}
|
||||
|
||||
return &ad, nil
|
||||
}
|
||||
|
||||
// AttestationData contains data attested by TPM commands (like Certify).
|
||||
type AttestationData struct {
|
||||
Magic uint32
|
||||
Type Tag
|
||||
QualifiedSigner Name
|
||||
ExtraData []byte
|
||||
ClockInfo ClockInfo
|
||||
FirmwareVersion uint64
|
||||
AttestedCertifyInfo *CertifyInfo
|
||||
AttestedQuoteInfo *QuoteInfo
|
||||
AttestedCreationInfo *CreationInfo
|
||||
}
|
||||
|
||||
// Tag is a command tag.
|
||||
type Tag uint16
|
||||
|
||||
type Name struct {
|
||||
Handle *Handle
|
||||
Digest *HashValue
|
||||
}
|
||||
|
||||
// A Handle is a reference to a TPM object.
|
||||
type Handle uint32
|
||||
type HashValue struct {
|
||||
Alg Algorithm
|
||||
Value []byte
|
||||
}
|
||||
|
||||
// ClockInfo contains TPM state info included in AttestationData.
|
||||
type ClockInfo struct {
|
||||
Clock uint64
|
||||
ResetCount uint32
|
||||
RestartCount uint32
|
||||
Safe byte
|
||||
}
|
||||
|
||||
// CertifyInfo contains Certify-specific data for TPMS_ATTEST.
|
||||
type CertifyInfo struct {
|
||||
Name Name
|
||||
QualifiedName Name
|
||||
}
|
||||
|
||||
// QuoteInfo represents a TPMS_QUOTE_INFO structure.
|
||||
type QuoteInfo struct {
|
||||
PCRSelection PCRSelection
|
||||
PCRDigest []byte
|
||||
}
|
||||
|
||||
// PCRSelection contains a slice of PCR indexes and a hash algorithm used in
|
||||
// them.
|
||||
type PCRSelection struct {
|
||||
Hash Algorithm
|
||||
PCRs []int
|
||||
}
|
||||
|
||||
// CreationInfo contains Creation-specific data for TPMS_ATTEST.
|
||||
type CreationInfo struct {
|
||||
Name Name
|
||||
// Most TPM2B_Digest structures contain a TPMU_HA structure
|
||||
// and get parsed to HashValue. This is never the case for the
|
||||
// digest in TPMS_CREATION_INFO.
|
||||
OpaqueDigest []byte
|
||||
}
|
||||
|
||||
func decodeName(in *bytes.Buffer) (*Name, error) {
|
||||
var nameBuf []byte
|
||||
if err := UnpackBuf(in, &nameBuf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
name := new(Name)
|
||||
switch len(nameBuf) {
|
||||
case 0:
|
||||
// No name is present.
|
||||
case 4:
|
||||
name.Handle = new(Handle)
|
||||
if err := UnpackBuf(bytes.NewBuffer(nameBuf), name.Handle); err != nil {
|
||||
return nil, fmt.Errorf("decoding Handle: %v", err)
|
||||
}
|
||||
default:
|
||||
var err error
|
||||
name.Digest, err = decodeHashValue(bytes.NewBuffer(nameBuf))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decoding Digest: %v", err)
|
||||
}
|
||||
}
|
||||
return name, nil
|
||||
}
|
||||
|
||||
func decodeHashValue(in *bytes.Buffer) (*HashValue, error) {
|
||||
var hv HashValue
|
||||
if err := UnpackBuf(in, &hv.Alg); err != nil {
|
||||
return nil, fmt.Errorf("decoding Alg: %v", err)
|
||||
}
|
||||
hfn, ok := hashConstructors[hv.Alg]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unsupported hash algorithm type 0x%x", hv.Alg)
|
||||
}
|
||||
hv.Value = make([]byte, hfn().Size())
|
||||
if _, err := in.Read(hv.Value); err != nil {
|
||||
return nil, fmt.Errorf("decoding Value: %v", err)
|
||||
}
|
||||
return &hv, nil
|
||||
}
|
||||
|
||||
// HashConstructor returns a function that can be used to make a
|
||||
// hash.Hash using the specified algorithm. An error is returned
|
||||
// if the algorithm is not a hash algorithm.
|
||||
func (a Algorithm) HashConstructor() (func() hash.Hash, error) {
|
||||
c, ok := hashConstructors[a]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("algorithm not supported: 0x%x", a)
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
var hashConstructors = map[Algorithm]func() hash.Hash{
|
||||
AlgSHA1: sha1.New,
|
||||
AlgSHA256: sha256.New,
|
||||
AlgSHA384: sha512.New384,
|
||||
AlgSHA512: sha512.New,
|
||||
}
|
||||
|
||||
// TPM Structure Tags. Tags are used to disambiguate structures, similar to Alg
|
||||
// values: tag value defines what kind of data lives in a nested field.
|
||||
const (
|
||||
TagNull Tag = 0x8000
|
||||
TagNoSessions Tag = 0x8001
|
||||
TagSessions Tag = 0x8002
|
||||
TagAttestCertify Tag = 0x8017
|
||||
TagAttestQuote Tag = 0x8018
|
||||
TagAttestCreation Tag = 0x801a
|
||||
TagHashCheck Tag = 0x8024
|
||||
)
|
||||
|
||||
func decodeCertifyInfo(in *bytes.Buffer) (*CertifyInfo, error) {
|
||||
var ci CertifyInfo
|
||||
|
||||
n, err := decodeName(in)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decoding Name: %v", err)
|
||||
}
|
||||
ci.Name = *n
|
||||
|
||||
n, err = decodeName(in)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decoding QualifiedName: %v", err)
|
||||
}
|
||||
ci.QualifiedName = *n
|
||||
|
||||
return &ci, nil
|
||||
}
|
||||
|
||||
func decodeCreationInfo(in *bytes.Buffer) (*CreationInfo, error) {
|
||||
var ci CreationInfo
|
||||
|
||||
n, err := decodeName(in)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decoding Name: %v", err)
|
||||
}
|
||||
ci.Name = *n
|
||||
|
||||
if err := UnpackBuf(in, &ci.OpaqueDigest); err != nil {
|
||||
return nil, fmt.Errorf("decoding Digest: %v", err)
|
||||
}
|
||||
|
||||
return &ci, nil
|
||||
}
|
||||
|
||||
func decodeQuoteInfo(in *bytes.Buffer) (*QuoteInfo, error) {
|
||||
var out QuoteInfo
|
||||
sel, err := decodeTPMLPCRSelection(in)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decoding PCRSelection: %v", err)
|
||||
}
|
||||
out.PCRSelection = sel
|
||||
if err := UnpackBuf(in, &out.PCRDigest); err != nil {
|
||||
return nil, fmt.Errorf("decoding PCRDigest: %v", err)
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
func decodeTPMLPCRSelection(buf *bytes.Buffer) (PCRSelection, error) {
|
||||
var count uint32
|
||||
var sel PCRSelection
|
||||
if err := UnpackBuf(buf, &count); err != nil {
|
||||
return sel, err
|
||||
}
|
||||
switch count {
|
||||
case 0:
|
||||
sel.Hash = AlgUnknown
|
||||
return sel, nil
|
||||
case 1: // We only support decoding of a single PCRSelection.
|
||||
default:
|
||||
return sel, fmt.Errorf("decoding TPML_PCR_SELECTION list longer than 1 is not supported (got length %d)", count)
|
||||
}
|
||||
|
||||
// See comment in encodeTPMLPCRSelection for details on this format.
|
||||
var ts tpmsPCRSelection
|
||||
if err := UnpackBuf(buf, &ts.Hash, &ts.Size); err != nil {
|
||||
return sel, err
|
||||
}
|
||||
ts.PCRs = make([]byte, ts.Size)
|
||||
if _, err := buf.Read(ts.PCRs); err != nil {
|
||||
return sel, err
|
||||
}
|
||||
|
||||
sel.Hash = ts.Hash
|
||||
for i := 0; i < int(ts.Size); i++ {
|
||||
for j := 0; j < 8; j++ {
|
||||
set := ts.PCRs[i] & byte(1<<byte(j))
|
||||
if set == 0 {
|
||||
continue
|
||||
}
|
||||
sel.PCRs = append(sel.PCRs, 8*i+j)
|
||||
}
|
||||
}
|
||||
return sel, nil
|
||||
}
|
||||
|
||||
type tpmsPCRSelection struct {
|
||||
Hash Algorithm
|
||||
Size byte
|
||||
PCRs RawBytes
|
||||
}
|
||||
|
||||
// RawBytes is for Pack and RunCommand arguments that are already encoded.
|
||||
// Compared to []byte, RawBytes will not be prepended with slice length during
|
||||
// encoding.
|
||||
type RawBytes []byte
|
||||
+152
@@ -0,0 +1,152 @@
|
||||
package googletpm
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// From github.com/google/go-tpm
|
||||
// Portions of existing package conflicted with existing build environment
|
||||
// and only needed very small amount of code for pubarea and certinfo structs
|
||||
// so copied them out to this package
|
||||
|
||||
// Supported Algorithms.
|
||||
const (
|
||||
AlgUnknown Algorithm = 0x0000
|
||||
AlgRSA Algorithm = 0x0001
|
||||
AlgSHA1 Algorithm = 0x0004
|
||||
AlgAES Algorithm = 0x0006
|
||||
AlgKeyedHash Algorithm = 0x0008
|
||||
AlgSHA256 Algorithm = 0x000B
|
||||
AlgSHA384 Algorithm = 0x000C
|
||||
AlgSHA512 Algorithm = 0x000D
|
||||
AlgNull Algorithm = 0x0010
|
||||
AlgRSASSA Algorithm = 0x0014
|
||||
AlgRSAES Algorithm = 0x0015
|
||||
AlgRSAPSS Algorithm = 0x0016
|
||||
AlgOAEP Algorithm = 0x0017
|
||||
AlgECDSA Algorithm = 0x0018
|
||||
AlgECDH Algorithm = 0x0019
|
||||
AlgECDAA Algorithm = 0x001A
|
||||
AlgKDF2 Algorithm = 0x0021
|
||||
AlgECC Algorithm = 0x0023
|
||||
AlgCTR Algorithm = 0x0040
|
||||
AlgOFB Algorithm = 0x0041
|
||||
AlgCBC Algorithm = 0x0042
|
||||
AlgCFB Algorithm = 0x0043
|
||||
AlgECB Algorithm = 0x0044
|
||||
)
|
||||
|
||||
// UnpackBuf recursively unpacks types from a reader just as encoding/binary
|
||||
// does under binary.BigEndian, but with one difference: it unpacks a byte
|
||||
// slice by first reading an integer with lengthPrefixSize bytes, then reading
|
||||
// that many bytes. It assumes that incoming values are pointers to values so
|
||||
// that, e.g., underlying slices can be resized as needed.
|
||||
func UnpackBuf(buf io.Reader, elts ...interface{}) error {
|
||||
for _, e := range elts {
|
||||
v := reflect.ValueOf(e)
|
||||
k := v.Kind()
|
||||
if k != reflect.Ptr {
|
||||
return fmt.Errorf("all values passed to Unpack must be pointers, got %v", k)
|
||||
}
|
||||
|
||||
if v.IsNil() {
|
||||
return errors.New("can't fill a nil pointer")
|
||||
}
|
||||
|
||||
iv := reflect.Indirect(v)
|
||||
switch iv.Kind() {
|
||||
case reflect.Struct:
|
||||
// Decompose the struct and copy over the values.
|
||||
for i := 0; i < iv.NumField(); i++ {
|
||||
if err := UnpackBuf(buf, iv.Field(i).Addr().Interface()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
case reflect.Slice:
|
||||
var size int
|
||||
_, isHandles := e.(*[]Handle)
|
||||
|
||||
switch {
|
||||
// []Handle always uses 2-byte length, even with TPM 1.2.
|
||||
case isHandles:
|
||||
var tmpSize uint16
|
||||
if err := binary.Read(buf, binary.BigEndian, &tmpSize); err != nil {
|
||||
return err
|
||||
}
|
||||
size = int(tmpSize)
|
||||
// TPM 2.0
|
||||
case lengthPrefixSize == tpm20PrefixSize:
|
||||
var tmpSize uint16
|
||||
if err := binary.Read(buf, binary.BigEndian, &tmpSize); err != nil {
|
||||
return err
|
||||
}
|
||||
size = int(tmpSize)
|
||||
// TPM 1.2
|
||||
case lengthPrefixSize == tpm12PrefixSize:
|
||||
var tmpSize uint32
|
||||
if err := binary.Read(buf, binary.BigEndian, &tmpSize); err != nil {
|
||||
return err
|
||||
}
|
||||
size = int(tmpSize)
|
||||
default:
|
||||
return fmt.Errorf("lengthPrefixSize is %d, must be either 2 or 4", lengthPrefixSize)
|
||||
}
|
||||
|
||||
// A zero size is used by the TPM to signal that certain elements
|
||||
// are not present.
|
||||
if size == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Make len(e) match size exactly.
|
||||
switch b := e.(type) {
|
||||
case *[]byte:
|
||||
if len(*b) >= size {
|
||||
*b = (*b)[:size]
|
||||
} else {
|
||||
*b = append(*b, make([]byte, size-len(*b))...)
|
||||
}
|
||||
case *[]Handle:
|
||||
if len(*b) >= size {
|
||||
*b = (*b)[:size]
|
||||
} else {
|
||||
*b = append(*b, make([]Handle, size-len(*b))...)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("can't fill pointer to %T, only []byte or []Handle slices", e)
|
||||
}
|
||||
|
||||
if err := binary.Read(buf, binary.BigEndian, e); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
if err := binary.Read(buf, binary.BigEndian, e); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// lengthPrefixSize is the size in bytes of length prefix for byte slices.
|
||||
//
|
||||
// In TPM 1.2 this is 4 bytes.
|
||||
// In TPM 2.0 this is 2 bytes.
|
||||
var lengthPrefixSize int
|
||||
|
||||
const (
|
||||
tpm12PrefixSize = 4
|
||||
tpm20PrefixSize = 2
|
||||
)
|
||||
|
||||
// UseTPM20LengthPrefixSize makes Pack/Unpack use TPM 2.0 encoding for byte
|
||||
// arrays.
|
||||
func UseTPM20LengthPrefixSize() {
|
||||
lengthPrefixSize = tpm20PrefixSize
|
||||
}
|
||||
+240
@@ -0,0 +1,240 @@
|
||||
package googletpm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"math/big"
|
||||
)
|
||||
|
||||
// DecodePublic decodes a TPMT_PUBLIC message. No error is returned if
|
||||
// the input has extra trailing data.
|
||||
func DecodePublic(buf []byte) (Public, error) {
|
||||
in := bytes.NewBuffer(buf)
|
||||
var pub Public
|
||||
var err error
|
||||
if err = UnpackBuf(in, &pub.Type, &pub.NameAlg, &pub.Attributes, &pub.AuthPolicy); err != nil {
|
||||
return pub, fmt.Errorf("decoding TPMT_PUBLIC: %v", err)
|
||||
}
|
||||
|
||||
switch pub.Type {
|
||||
case AlgRSA:
|
||||
pub.RSAParameters, err = decodeRSAParams(in)
|
||||
case AlgECC:
|
||||
pub.ECCParameters, err = decodeECCParams(in)
|
||||
default:
|
||||
err = fmt.Errorf("unsupported type in TPMT_PUBLIC: %v", pub.Type)
|
||||
}
|
||||
return pub, err
|
||||
}
|
||||
|
||||
// Public contains the public area of an object.
|
||||
type Public struct {
|
||||
Type Algorithm
|
||||
NameAlg Algorithm
|
||||
Attributes KeyProp
|
||||
AuthPolicy []byte
|
||||
|
||||
// If Type is AlgKeyedHash, then do not set these.
|
||||
// Otherwise, only one of the Parameters fields should be set. When encoding/decoding,
|
||||
// one will be picked based on Type.
|
||||
RSAParameters *RSAParams
|
||||
ECCParameters *ECCParams
|
||||
}
|
||||
|
||||
// Algorithm represents a TPM_ALG_ID value.
|
||||
type Algorithm uint16
|
||||
|
||||
// KeyProp is a bitmask used in Attributes field of key templates. Individual
|
||||
// flags should be OR-ed to form a full mask.
|
||||
type KeyProp uint32
|
||||
|
||||
// Key properties.
|
||||
const (
|
||||
FlagFixedTPM KeyProp = 0x00000002
|
||||
FlagFixedParent KeyProp = 0x00000010
|
||||
FlagSensitiveDataOrigin KeyProp = 0x00000020
|
||||
FlagUserWithAuth KeyProp = 0x00000040
|
||||
FlagAdminWithPolicy KeyProp = 0x00000080
|
||||
FlagNoDA KeyProp = 0x00000400
|
||||
FlagRestricted KeyProp = 0x00010000
|
||||
FlagDecrypt KeyProp = 0x00020000
|
||||
FlagSign KeyProp = 0x00040000
|
||||
|
||||
FlagSealDefault = FlagFixedTPM | FlagFixedParent
|
||||
FlagSignerDefault = FlagSign | FlagRestricted | FlagFixedTPM |
|
||||
FlagFixedParent | FlagSensitiveDataOrigin | FlagUserWithAuth
|
||||
FlagStorageDefault = FlagDecrypt | FlagRestricted | FlagFixedTPM |
|
||||
FlagFixedParent | FlagSensitiveDataOrigin | FlagUserWithAuth
|
||||
)
|
||||
|
||||
func decodeRSAParams(in *bytes.Buffer) (*RSAParams, error) {
|
||||
var params RSAParams
|
||||
var err error
|
||||
|
||||
if params.Symmetric, err = decodeSymScheme(in); err != nil {
|
||||
return nil, fmt.Errorf("decoding Symmetric: %v", err)
|
||||
}
|
||||
if params.Sign, err = decodeSigScheme(in); err != nil {
|
||||
return nil, fmt.Errorf("decoding Sign: %v", err)
|
||||
}
|
||||
var modBytes []byte
|
||||
if err := UnpackBuf(in, ¶ms.KeyBits, ¶ms.Exponent, &modBytes); err != nil {
|
||||
return nil, fmt.Errorf("decoding KeyBits, Exponent, Modulus: %v", err)
|
||||
}
|
||||
if params.Exponent == 0 {
|
||||
params.encodeDefaultExponentAsZero = true
|
||||
params.Exponent = defaultRSAExponent
|
||||
}
|
||||
params.Modulus = new(big.Int).SetBytes(modBytes)
|
||||
return ¶ms, nil
|
||||
}
|
||||
|
||||
const defaultRSAExponent = 1<<16 + 1
|
||||
|
||||
// RSAParams represents parameters of an RSA key pair.
|
||||
//
|
||||
// Symmetric and Sign may be nil, depending on key Attributes in Public.
|
||||
//
|
||||
// One of Modulus and ModulusRaw must always be non-nil. Modulus takes
|
||||
// precedence. ModulusRaw is used for key templates where the field named
|
||||
// "unique" must be a byte array of all zeroes.
|
||||
type RSAParams struct {
|
||||
Symmetric *SymScheme
|
||||
Sign *SigScheme
|
||||
KeyBits uint16
|
||||
// The default Exponent (65537) has two representations; the
|
||||
// 0 value, and the value 65537.
|
||||
// If encodeDefaultExponentAsZero is set, an exponent of 65537
|
||||
// will be encoded as zero. This is necessary to produce an identical
|
||||
// encoded bitstream, so Name digest calculations will be correct.
|
||||
encodeDefaultExponentAsZero bool
|
||||
Exponent uint32
|
||||
ModulusRaw []byte
|
||||
Modulus *big.Int
|
||||
}
|
||||
|
||||
// SymScheme represents a symmetric encryption scheme.
|
||||
type SymScheme struct {
|
||||
Alg Algorithm
|
||||
KeyBits uint16
|
||||
Mode Algorithm
|
||||
} // SigScheme represents a signing scheme.
|
||||
type SigScheme struct {
|
||||
Alg Algorithm
|
||||
Hash Algorithm
|
||||
Count uint32
|
||||
}
|
||||
|
||||
func decodeSigScheme(in *bytes.Buffer) (*SigScheme, error) {
|
||||
var scheme SigScheme
|
||||
if err := UnpackBuf(in, &scheme.Alg); err != nil {
|
||||
return nil, fmt.Errorf("decoding Alg: %v", err)
|
||||
}
|
||||
if scheme.Alg == AlgNull {
|
||||
return nil, nil
|
||||
}
|
||||
if err := UnpackBuf(in, &scheme.Hash); err != nil {
|
||||
return nil, fmt.Errorf("decoding Hash: %v", err)
|
||||
}
|
||||
if scheme.Alg.UsesCount() {
|
||||
if err := UnpackBuf(in, &scheme.Count); err != nil {
|
||||
return nil, fmt.Errorf("decoding Count: %v", err)
|
||||
}
|
||||
}
|
||||
return &scheme, nil
|
||||
}
|
||||
|
||||
// UsesCount returns true if a signature algorithm uses count value.
|
||||
func (a Algorithm) UsesCount() bool {
|
||||
return a == AlgECDAA
|
||||
}
|
||||
|
||||
func decodeKDFScheme(in *bytes.Buffer) (*KDFScheme, error) {
|
||||
var scheme KDFScheme
|
||||
if err := UnpackBuf(in, &scheme.Alg); err != nil {
|
||||
return nil, fmt.Errorf("decoding Alg: %v", err)
|
||||
}
|
||||
if scheme.Alg == AlgNull {
|
||||
return nil, nil
|
||||
}
|
||||
if err := UnpackBuf(in, &scheme.Hash); err != nil {
|
||||
return nil, fmt.Errorf("decoding Hash: %v", err)
|
||||
}
|
||||
return &scheme, nil
|
||||
}
|
||||
func decodeSymScheme(in *bytes.Buffer) (*SymScheme, error) {
|
||||
var scheme SymScheme
|
||||
if err := UnpackBuf(in, &scheme.Alg); err != nil {
|
||||
return nil, fmt.Errorf("decoding Alg: %v", err)
|
||||
}
|
||||
if scheme.Alg == AlgNull {
|
||||
return nil, nil
|
||||
}
|
||||
if err := UnpackBuf(in, &scheme.KeyBits, &scheme.Mode); err != nil {
|
||||
return nil, fmt.Errorf("decoding KeyBits, Mode: %v", err)
|
||||
}
|
||||
return &scheme, nil
|
||||
}
|
||||
func decodeECCParams(in *bytes.Buffer) (*ECCParams, error) {
|
||||
var params ECCParams
|
||||
var err error
|
||||
|
||||
if params.Symmetric, err = decodeSymScheme(in); err != nil {
|
||||
return nil, fmt.Errorf("decoding Symmetric: %v", err)
|
||||
}
|
||||
if params.Sign, err = decodeSigScheme(in); err != nil {
|
||||
return nil, fmt.Errorf("decoding Sign: %v", err)
|
||||
}
|
||||
if err := UnpackBuf(in, ¶ms.CurveID); err != nil {
|
||||
return nil, fmt.Errorf("decoding CurveID: %v", err)
|
||||
}
|
||||
if params.KDF, err = decodeKDFScheme(in); err != nil {
|
||||
return nil, fmt.Errorf("decoding KDF: %v", err)
|
||||
}
|
||||
var x, y []byte
|
||||
if err := UnpackBuf(in, &x, &y); err != nil {
|
||||
return nil, fmt.Errorf("decoding Point: %v", err)
|
||||
}
|
||||
params.Point.X = new(big.Int).SetBytes(x)
|
||||
params.Point.Y = new(big.Int).SetBytes(y)
|
||||
return ¶ms, nil
|
||||
}
|
||||
|
||||
// ECCParams represents parameters of an ECC key pair.
|
||||
//
|
||||
// Symmetric, Sign and KDF may be nil, depending on key Attributes in Public.
|
||||
type ECCParams struct {
|
||||
Symmetric *SymScheme
|
||||
Sign *SigScheme
|
||||
CurveID EllipticCurve
|
||||
KDF *KDFScheme
|
||||
Point ECPoint
|
||||
}
|
||||
|
||||
// EllipticCurve identifies specific EC curves.
|
||||
type EllipticCurve uint16
|
||||
|
||||
// ECC curves supported by TPM 2.0 spec.
|
||||
const (
|
||||
CurveNISTP192 = EllipticCurve(iota + 1)
|
||||
CurveNISTP224
|
||||
CurveNISTP256
|
||||
CurveNISTP384
|
||||
CurveNISTP521
|
||||
|
||||
CurveBNP256 = EllipticCurve(iota + 10)
|
||||
CurveBNP638
|
||||
|
||||
CurveSM2P256 = EllipticCurve(0x0020)
|
||||
)
|
||||
|
||||
// ECPoint represents a ECC coordinates for a point.
|
||||
type ECPoint struct {
|
||||
X, Y *big.Int
|
||||
}
|
||||
|
||||
// KDFScheme represents a KDF (Key Derivation Function) scheme.
|
||||
type KDFScheme struct {
|
||||
Alg Algorithm
|
||||
Hash Algorithm
|
||||
}
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"github.com/duo-labs/webauthn/protocol/webauthncose"
|
||||
)
|
||||
|
||||
type CredentialCreation struct {
|
||||
Response PublicKeyCredentialCreationOptions `json:"publicKey"`
|
||||
}
|
||||
|
||||
type CredentialAssertion struct {
|
||||
Response PublicKeyCredentialRequestOptions `json:"publicKey"`
|
||||
}
|
||||
|
||||
// In order to create a Credential via create(), the caller specifies a few parameters in a CredentialCreationOptions object.
|
||||
// See §5.4. Options for Credential Creation https://www.w3.org/TR/webauthn/#dictionary-makecredentialoptions
|
||||
type PublicKeyCredentialCreationOptions struct {
|
||||
Challenge Challenge `json:"challenge"`
|
||||
RelyingParty RelyingPartyEntity `json:"rp"`
|
||||
User UserEntity `json:"user"`
|
||||
Parameters []CredentialParameter `json:"pubKeyCredParams,omitempty"`
|
||||
AuthenticatorSelection AuthenticatorSelection `json:"authenticatorSelection,omitempty"`
|
||||
Timeout int `json:"timeout,omitempty"`
|
||||
CredentialExcludeList []CredentialDescriptor `json:"excludeCredentials,omitempty"`
|
||||
Extensions AuthenticationExtensions `json:"extensions,omitempty"`
|
||||
Attestation ConveyancePreference `json:"attestation,omitempty"`
|
||||
}
|
||||
|
||||
// The PublicKeyCredentialRequestOptions dictionary supplies get() with the data it needs to generate an assertion.
|
||||
// Its challenge member MUST be present, while its other members are OPTIONAL.
|
||||
// See §5.5. Options for Assertion Generation https://www.w3.org/TR/webauthn/#assertion-options
|
||||
type PublicKeyCredentialRequestOptions struct {
|
||||
Challenge Challenge `json:"challenge"`
|
||||
Timeout int `json:"timeout,omitempty"`
|
||||
RelyingPartyID string `json:"rpId,omitempty"`
|
||||
AllowedCredentials []CredentialDescriptor `json:"allowCredentials,omitempty"`
|
||||
UserVerification UserVerificationRequirement `json:"userVerification,omitempty"` // Default is "preferred"
|
||||
Extensions AuthenticationExtensions `json:"extensions,omitempty"`
|
||||
}
|
||||
|
||||
// This dictionary contains the attributes that are specified by a caller when referring to a public
|
||||
// key credential as an input parameter to the create() or get() methods. It mirrors the fields of
|
||||
// the PublicKeyCredential object returned by the latter methods.
|
||||
// See §5.10.3. Credential Descriptor https://www.w3.org/TR/webauthn/#credential-dictionary
|
||||
type CredentialDescriptor struct {
|
||||
// The valid credential types.
|
||||
Type CredentialType `json:"type"`
|
||||
// CredentialID The ID of a credential to allow/disallow
|
||||
CredentialID []byte `json:"id"`
|
||||
// The authenticator transports that can be used
|
||||
Transport []AuthenticatorTransport `json:"transports,omitempty"`
|
||||
}
|
||||
|
||||
// CredentialParameter is the credential type and algorithm
|
||||
// that the relying party wants the authenticator to create
|
||||
type CredentialParameter struct {
|
||||
Type CredentialType `json:"type"`
|
||||
Algorithm webauthncose.COSEAlgorithmIdentifier `json:"alg"`
|
||||
}
|
||||
|
||||
// This enumeration defines the valid credential types.
|
||||
// It is an extension point; values can be added to it in the future, as
|
||||
// more credential types are defined. The values of this enumeration are used
|
||||
// for versioning the Authentication Assertion and attestation structures according
|
||||
// to the type of the authenticator.
|
||||
// See §5.10.3. Credential Descriptor https://www.w3.org/TR/webauthn/#credentialType
|
||||
type CredentialType string
|
||||
|
||||
const (
|
||||
// PublicKeyCredentialType - Currently one credential type is defined, namely "public-key".
|
||||
PublicKeyCredentialType CredentialType = "public-key"
|
||||
)
|
||||
|
||||
// AuthenticationExtensions - referred to as AuthenticationExtensionsClientInputs in the
|
||||
// spec document, this member contains additional parameters requesting additional processing
|
||||
// by the client and authenticator.
|
||||
// This is currently under development
|
||||
type AuthenticationExtensions map[string]interface{}
|
||||
|
||||
// WebAuthn Relying Parties may use the AuthenticatorSelectionCriteria dictionary to specify their requirements
|
||||
// regarding authenticator attributes. See §5.4.4. Authenticator Selection Criteria
|
||||
// https://www.w3.org/TR/webauthn/#authenticatorSelection
|
||||
type AuthenticatorSelection struct {
|
||||
// AuthenticatorAttachment If this member is present, eligible authenticators are filtered to only
|
||||
// authenticators attached with the specified AuthenticatorAttachment enum
|
||||
AuthenticatorAttachment AuthenticatorAttachment `json:"authenticatorAttachment,omitempty"`
|
||||
// RequireResidentKey this member describes the Relying Party's requirements regarding resident
|
||||
// credentials. If the parameter is set to true, the authenticator MUST create a client-side-resident
|
||||
// public key credential source when creating a public key credential.
|
||||
RequireResidentKey *bool `json:"requireResidentKey,omitempty"`
|
||||
// UserVerification This member describes the Relying Party's requirements regarding user verification for
|
||||
// the create() operation. Eligible authenticators are filtered to only those capable of satisfying this
|
||||
// requirement.
|
||||
UserVerification UserVerificationRequirement `json:"userVerification,omitempty"`
|
||||
}
|
||||
|
||||
// WebAuthn Relying Parties may use AttestationConveyancePreference to specify their preference regarding
|
||||
// attestation conveyance during credential generation. See §5.4.6. https://www.w3.org/TR/webauthn/#attestation-convey
|
||||
type ConveyancePreference string
|
||||
|
||||
const (
|
||||
// The default value. This value indicates that the Relying Party is not interested in authenticator attestation. For example,
|
||||
// in order to potentially avoid having to obtain user consent to relay identifying information to the Relying Party, or to
|
||||
// save a roundtrip to an Attestation CA.
|
||||
PreferNoAttestation ConveyancePreference = "none"
|
||||
// This value indicates that the Relying Party prefers an attestation conveyance yielding verifiable attestation
|
||||
// statements, but allows the client to decide how to obtain such attestation statements. The client MAY replace
|
||||
// the authenticator-generated attestation statements with attestation statements generated by an Anonymization
|
||||
// CA, in order to protect the user’s privacy, or to assist Relying Parties with attestation verification in a
|
||||
// heterogeneous ecosystem.
|
||||
PreferIndirectAttestation ConveyancePreference = "indirect"
|
||||
// This value indicates that the Relying Party wants to receive the attestation statement as generated by the authenticator.
|
||||
PreferDirectAttestation ConveyancePreference = "direct"
|
||||
)
|
||||
|
||||
func (a *PublicKeyCredentialRequestOptions) GetAllowedCredentialIDs() [][]byte {
|
||||
var allowedCredentialIDs = make([][]byte, len(a.AllowedCredentials))
|
||||
for i, credential := range a.AllowedCredentials {
|
||||
allowedCredentialIDs[i] = credential.CredentialID
|
||||
}
|
||||
return allowedCredentialIDs
|
||||
}
|
||||
|
||||
type Extensions interface{}
|
||||
|
||||
type ServerResponse struct {
|
||||
Status ServerResponseStatus `json:"status"`
|
||||
Message string `json:"errorMessage"`
|
||||
}
|
||||
|
||||
type ServerResponseStatus string
|
||||
|
||||
const (
|
||||
StatusOk ServerResponseStatus = "ok"
|
||||
StatusFailed ServerResponseStatus = "failed"
|
||||
)
|
||||
+1
@@ -0,0 +1 @@
|
||||
package protocol
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
// +build go1.13
|
||||
|
||||
package webauthncose
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"crypto/x509"
|
||||
)
|
||||
|
||||
func marshalEd25519PublicKey(pub ed25519.PublicKey) ([]byte, error) {
|
||||
return x509.MarshalPKIXPublicKey(pub)
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
// +build !go1.13
|
||||
|
||||
package webauthncose
|
||||
|
||||
import (
|
||||
"crypto/x509/pkix"
|
||||
"encoding/asn1"
|
||||
|
||||
"golang.org/x/crypto/ed25519"
|
||||
)
|
||||
|
||||
var oidSignatureEd25519 = asn1.ObjectIdentifier{1, 3, 101, 112}
|
||||
|
||||
type pkixPublicKey struct {
|
||||
Algo pkix.AlgorithmIdentifier
|
||||
BitString asn1.BitString
|
||||
}
|
||||
|
||||
// marshalEd25519PublicKey is a backport of the functionality introduced in
|
||||
// Go v1.13.
|
||||
// Ref: https://golang.org/doc/go1.13#crypto/ed25519
|
||||
// Ref: https://golang.org/doc/go1.13#crypto/x509
|
||||
func marshalEd25519PublicKey(pub ed25519.PublicKey) ([]byte, error) {
|
||||
publicKeyBytes := pub
|
||||
var publicKeyAlgorithm pkix.AlgorithmIdentifier
|
||||
publicKeyAlgorithm.Algorithm = oidSignatureEd25519
|
||||
|
||||
pkix := pkixPublicKey{
|
||||
Algo: publicKeyAlgorithm,
|
||||
BitString: asn1.BitString{
|
||||
Bytes: publicKeyBytes,
|
||||
BitLength: 8 * len(publicKeyBytes),
|
||||
},
|
||||
}
|
||||
|
||||
ret, _ := asn1.Marshal(pkix)
|
||||
return ret, nil
|
||||
}
|
||||
+400
@@ -0,0 +1,400 @@
|
||||
package webauthncose
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/asn1"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"hash"
|
||||
"math/big"
|
||||
|
||||
"github.com/fxamacker/cbor/v2"
|
||||
"golang.org/x/crypto/ed25519"
|
||||
)
|
||||
|
||||
// PublicKeyData The public key portion of a Relying Party-specific credential key pair, generated
|
||||
// by an authenticator and returned to a Relying Party at registration time. We unpack this object
|
||||
// using fxamacker's cbor library ("github.com/fxamacker/cbor/v2") which is why there are cbor tags
|
||||
// included. The tag field values correspond to the IANA COSE keys that give their respective
|
||||
// values.
|
||||
// See §6.4.1.1 https://www.w3.org/TR/webauthn/#sctn-encoded-credPubKey-examples for examples of this
|
||||
// COSE data.
|
||||
type PublicKeyData struct {
|
||||
// Decode the results to int by default.
|
||||
_struct bool `cbor:",keyasint" json:"public_key"`
|
||||
// The type of key created. Should be OKP, EC2, or RSA.
|
||||
KeyType int64 `cbor:"1,keyasint" json:"kty"`
|
||||
// A COSEAlgorithmIdentifier for the algorithm used to derive the key signature.
|
||||
Algorithm int64 `cbor:"3,keyasint" json:"alg"`
|
||||
}
|
||||
type EC2PublicKeyData struct {
|
||||
PublicKeyData
|
||||
// If the key type is EC2, the curve on which we derive the signature from.
|
||||
Curve int64 `cbor:"-1,keyasint,omitempty" json:"crv"`
|
||||
// A byte string 32 bytes in length that holds the x coordinate of the key.
|
||||
XCoord []byte `cbor:"-2,keyasint,omitempty" json:"x"`
|
||||
// A byte string 32 bytes in length that holds the y coordinate of the key.
|
||||
YCoord []byte `cbor:"-3,keyasint,omitempty" json:"y"`
|
||||
}
|
||||
|
||||
type RSAPublicKeyData struct {
|
||||
PublicKeyData
|
||||
// Represents the modulus parameter for the RSA algorithm
|
||||
Modulus []byte `cbor:"-1,keyasint,omitempty" json:"n"`
|
||||
// Represents the exponent parameter for the RSA algorithm
|
||||
Exponent []byte `cbor:"-2,keyasint,omitempty" json:"e"`
|
||||
}
|
||||
|
||||
type OKPPublicKeyData struct {
|
||||
PublicKeyData
|
||||
Curve int64
|
||||
// A byte string that holds the x coordinate of the key.
|
||||
XCoord []byte `cbor:"-2,keyasint,omitempty" json:"x"`
|
||||
}
|
||||
|
||||
// Verify Octet Key Pair (OKP) Public Key Signature
|
||||
func (k *OKPPublicKeyData) Verify(data []byte, sig []byte) (bool, error) {
|
||||
var key ed25519.PublicKey = make([]byte, ed25519.PublicKeySize)
|
||||
copy(key, k.XCoord)
|
||||
return ed25519.Verify(key, data, sig), nil
|
||||
}
|
||||
|
||||
// Verify Elliptic Curce Public Key Signature
|
||||
func (k *EC2PublicKeyData) Verify(data []byte, sig []byte) (bool, error) {
|
||||
var curve elliptic.Curve
|
||||
switch COSEAlgorithmIdentifier(k.Algorithm) {
|
||||
case AlgES512: // IANA COSE code for ECDSA w/ SHA-512
|
||||
curve = elliptic.P521()
|
||||
case AlgES384: // IANA COSE code for ECDSA w/ SHA-384
|
||||
curve = elliptic.P384()
|
||||
case AlgES256: // IANA COSE code for ECDSA w/ SHA-256
|
||||
curve = elliptic.P256()
|
||||
default:
|
||||
return false, ErrUnsupportedAlgorithm
|
||||
}
|
||||
|
||||
pubkey := &ecdsa.PublicKey{
|
||||
Curve: curve,
|
||||
X: big.NewInt(0).SetBytes(k.XCoord),
|
||||
Y: big.NewInt(0).SetBytes(k.YCoord),
|
||||
}
|
||||
|
||||
type ECDSASignature struct {
|
||||
R, S *big.Int
|
||||
}
|
||||
|
||||
e := &ECDSASignature{}
|
||||
f := HasherFromCOSEAlg(COSEAlgorithmIdentifier(k.PublicKeyData.Algorithm))
|
||||
h := f()
|
||||
h.Write(data)
|
||||
_, err := asn1.Unmarshal(sig, e)
|
||||
if err != nil {
|
||||
return false, ErrSigNotProvidedOrInvalid
|
||||
}
|
||||
return ecdsa.Verify(pubkey, h.Sum(nil), e.R, e.S), nil
|
||||
}
|
||||
|
||||
// Verify RSA Public Key Signature
|
||||
func (k *RSAPublicKeyData) Verify(data []byte, sig []byte) (bool, error) {
|
||||
pubkey := &rsa.PublicKey{
|
||||
N: big.NewInt(0).SetBytes(k.Modulus),
|
||||
E: int(uint(k.Exponent[2]) | uint(k.Exponent[1])<<8 | uint(k.Exponent[0])<<16),
|
||||
}
|
||||
|
||||
f := HasherFromCOSEAlg(COSEAlgorithmIdentifier(k.PublicKeyData.Algorithm))
|
||||
h := f()
|
||||
h.Write(data)
|
||||
|
||||
var hash crypto.Hash
|
||||
switch COSEAlgorithmIdentifier(k.PublicKeyData.Algorithm) {
|
||||
case AlgRS1:
|
||||
hash = crypto.SHA1
|
||||
case AlgPS256, AlgRS256:
|
||||
hash = crypto.SHA256
|
||||
case AlgPS384, AlgRS384:
|
||||
hash = crypto.SHA384
|
||||
case AlgPS512, AlgRS512:
|
||||
hash = crypto.SHA512
|
||||
default:
|
||||
return false, ErrUnsupportedAlgorithm
|
||||
}
|
||||
switch COSEAlgorithmIdentifier(k.PublicKeyData.Algorithm) {
|
||||
case AlgPS256, AlgPS384, AlgPS512:
|
||||
err := rsa.VerifyPSS(pubkey, hash, h.Sum(nil), sig, nil)
|
||||
return err == nil, err
|
||||
|
||||
case AlgRS1, AlgRS256, AlgRS384, AlgRS512:
|
||||
err := rsa.VerifyPKCS1v15(pubkey, hash, h.Sum(nil), sig)
|
||||
return err == nil, err
|
||||
default:
|
||||
return false, ErrUnsupportedAlgorithm
|
||||
}
|
||||
}
|
||||
|
||||
// Return which signature algorithm is being used from the COSE Key
|
||||
func SigAlgFromCOSEAlg(coseAlg COSEAlgorithmIdentifier) SignatureAlgorithm {
|
||||
for _, details := range SignatureAlgorithmDetails {
|
||||
if details.coseAlg == coseAlg {
|
||||
return details.algo
|
||||
}
|
||||
}
|
||||
return UnknownSignatureAlgorithm
|
||||
}
|
||||
|
||||
// Return the Hashing interface to be used for a given COSE Algorithm
|
||||
func HasherFromCOSEAlg(coseAlg COSEAlgorithmIdentifier) func() hash.Hash {
|
||||
for _, details := range SignatureAlgorithmDetails {
|
||||
if details.coseAlg == coseAlg {
|
||||
return details.hasher
|
||||
}
|
||||
}
|
||||
// default to SHA256? Why not.
|
||||
return crypto.SHA256.New
|
||||
}
|
||||
|
||||
// Figure out what kind of COSE material was provided and create the data for the new key
|
||||
func ParsePublicKey(keyBytes []byte) (interface{}, error) {
|
||||
pk := PublicKeyData{}
|
||||
cbor.Unmarshal(keyBytes, &pk)
|
||||
switch COSEKeyType(pk.KeyType) {
|
||||
case OctetKey:
|
||||
var o OKPPublicKeyData
|
||||
cbor.Unmarshal(keyBytes, &o)
|
||||
o.PublicKeyData = pk
|
||||
return o, nil
|
||||
case EllipticKey:
|
||||
var e EC2PublicKeyData
|
||||
cbor.Unmarshal(keyBytes, &e)
|
||||
e.PublicKeyData = pk
|
||||
return e, nil
|
||||
case RSAKey:
|
||||
var r RSAPublicKeyData
|
||||
cbor.Unmarshal(keyBytes, &r)
|
||||
r.PublicKeyData = pk
|
||||
return r, nil
|
||||
default:
|
||||
return nil, ErrUnsupportedKey
|
||||
}
|
||||
}
|
||||
|
||||
// ParseFIDOPublicKey is only used when the appID extension is configured by the assertion response.
|
||||
func ParseFIDOPublicKey(keyBytes []byte) (EC2PublicKeyData, error) {
|
||||
x, y := elliptic.Unmarshal(elliptic.P256(), keyBytes)
|
||||
|
||||
return EC2PublicKeyData{
|
||||
PublicKeyData: PublicKeyData{
|
||||
Algorithm: int64(AlgES256),
|
||||
},
|
||||
XCoord: x.Bytes(),
|
||||
YCoord: y.Bytes(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// COSEAlgorithmIdentifier From §5.10.5. A number identifying a cryptographic algorithm. The algorithm
|
||||
// identifiers SHOULD be values registered in the IANA COSE Algorithms registry
|
||||
// [https://www.w3.org/TR/webauthn/#biblio-iana-cose-algs-reg], for instance, -7 for "ES256"
|
||||
// and -257 for "RS256".
|
||||
type COSEAlgorithmIdentifier int
|
||||
|
||||
const (
|
||||
// AlgES256 ECDSA with SHA-256
|
||||
AlgES256 COSEAlgorithmIdentifier = -7
|
||||
// AlgES384 ECDSA with SHA-384
|
||||
AlgES384 COSEAlgorithmIdentifier = -35
|
||||
// AlgES512 ECDSA with SHA-512
|
||||
AlgES512 COSEAlgorithmIdentifier = -36
|
||||
// AlgRS1 RSASSA-PKCS1-v1_5 with SHA-1
|
||||
AlgRS1 COSEAlgorithmIdentifier = -65535
|
||||
// AlgRS256 RSASSA-PKCS1-v1_5 with SHA-256
|
||||
AlgRS256 COSEAlgorithmIdentifier = -257
|
||||
// AlgRS384 RSASSA-PKCS1-v1_5 with SHA-384
|
||||
AlgRS384 COSEAlgorithmIdentifier = -258
|
||||
// AlgRS512 RSASSA-PKCS1-v1_5 with SHA-512
|
||||
AlgRS512 COSEAlgorithmIdentifier = -259
|
||||
// AlgPS256 RSASSA-PSS with SHA-256
|
||||
AlgPS256 COSEAlgorithmIdentifier = -37
|
||||
// AlgPS384 RSASSA-PSS with SHA-384
|
||||
AlgPS384 COSEAlgorithmIdentifier = -38
|
||||
// AlgPS512 RSASSA-PSS with SHA-512
|
||||
AlgPS512 COSEAlgorithmIdentifier = -39
|
||||
// AlgEdDSA EdDSA
|
||||
AlgEdDSA COSEAlgorithmIdentifier = -8
|
||||
)
|
||||
|
||||
// The Key Type derived from the IANA COSE AuthData
|
||||
type COSEKeyType int
|
||||
|
||||
const (
|
||||
// OctetKey is an Octet Key
|
||||
OctetKey COSEKeyType = 1
|
||||
// EllipticKey is an Elliptic Curve Public Key
|
||||
EllipticKey COSEKeyType = 2
|
||||
// RSAKey is an RSA Public Key
|
||||
RSAKey COSEKeyType = 3
|
||||
)
|
||||
|
||||
func VerifySignature(key interface{}, data []byte, sig []byte) (bool, error) {
|
||||
|
||||
switch key.(type) {
|
||||
case OKPPublicKeyData:
|
||||
o := key.(OKPPublicKeyData)
|
||||
return o.Verify(data, sig)
|
||||
case EC2PublicKeyData:
|
||||
e := key.(EC2PublicKeyData)
|
||||
return e.Verify(data, sig)
|
||||
case RSAPublicKeyData:
|
||||
r := key.(RSAPublicKeyData)
|
||||
return r.Verify(data, sig)
|
||||
default:
|
||||
return false, ErrUnsupportedKey
|
||||
}
|
||||
}
|
||||
|
||||
func DisplayPublicKey(cpk []byte) string {
|
||||
parsedKey, err := ParsePublicKey(cpk)
|
||||
if err != nil {
|
||||
return "Cannot display key"
|
||||
}
|
||||
switch parsedKey.(type) {
|
||||
case RSAPublicKeyData:
|
||||
pKey := parsedKey.(RSAPublicKeyData)
|
||||
rKey := &rsa.PublicKey{
|
||||
N: big.NewInt(0).SetBytes(pKey.Modulus),
|
||||
E: int(uint(pKey.Exponent[2]) | uint(pKey.Exponent[1])<<8 | uint(pKey.Exponent[0])<<16),
|
||||
}
|
||||
data, err := x509.MarshalPKIXPublicKey(rKey)
|
||||
if err != nil {
|
||||
return "Cannot display key"
|
||||
}
|
||||
pemBytes := pem.EncodeToMemory(&pem.Block{
|
||||
Type: "RSA PUBLIC KEY",
|
||||
Bytes: data,
|
||||
})
|
||||
return fmt.Sprintf("%s", pemBytes)
|
||||
case EC2PublicKeyData:
|
||||
pKey := parsedKey.(EC2PublicKeyData)
|
||||
var curve elliptic.Curve
|
||||
switch COSEAlgorithmIdentifier(pKey.Algorithm) {
|
||||
case AlgES256:
|
||||
curve = elliptic.P256()
|
||||
case AlgES384:
|
||||
curve = elliptic.P384()
|
||||
case AlgES512:
|
||||
curve = elliptic.P521()
|
||||
default:
|
||||
return "Cannot display key"
|
||||
}
|
||||
eKey := &ecdsa.PublicKey{
|
||||
Curve: curve,
|
||||
X: big.NewInt(0).SetBytes(pKey.XCoord),
|
||||
Y: big.NewInt(0).SetBytes(pKey.YCoord),
|
||||
}
|
||||
data, err := x509.MarshalPKIXPublicKey(eKey)
|
||||
if err != nil {
|
||||
return "Cannot display key"
|
||||
}
|
||||
pemBytes := pem.EncodeToMemory(&pem.Block{
|
||||
Type: "PUBLIC KEY",
|
||||
Bytes: data,
|
||||
})
|
||||
return fmt.Sprintf("%s", pemBytes)
|
||||
case OKPPublicKeyData:
|
||||
pKey := parsedKey.(OKPPublicKeyData)
|
||||
if len(pKey.XCoord) != ed25519.PublicKeySize {
|
||||
return "Cannot display key"
|
||||
}
|
||||
var oKey ed25519.PublicKey = make([]byte, ed25519.PublicKeySize)
|
||||
copy(oKey, pKey.XCoord)
|
||||
data, err := marshalEd25519PublicKey(oKey)
|
||||
if err != nil {
|
||||
return "Cannot display key"
|
||||
}
|
||||
pemBytes := pem.EncodeToMemory(&pem.Block{
|
||||
Type: "PUBLIC KEY",
|
||||
Bytes: data,
|
||||
})
|
||||
return fmt.Sprintf("%s", pemBytes)
|
||||
|
||||
default:
|
||||
return "Cannot display key of this type"
|
||||
}
|
||||
}
|
||||
|
||||
// Algorithm enumerations used for
|
||||
type SignatureAlgorithm int
|
||||
|
||||
const (
|
||||
UnknownSignatureAlgorithm SignatureAlgorithm = iota
|
||||
MD2WithRSA
|
||||
MD5WithRSA
|
||||
SHA1WithRSA
|
||||
SHA256WithRSA
|
||||
SHA384WithRSA
|
||||
SHA512WithRSA
|
||||
DSAWithSHA1
|
||||
DSAWithSHA256
|
||||
ECDSAWithSHA1
|
||||
ECDSAWithSHA256
|
||||
ECDSAWithSHA384
|
||||
ECDSAWithSHA512
|
||||
SHA256WithRSAPSS
|
||||
SHA384WithRSAPSS
|
||||
SHA512WithRSAPSS
|
||||
)
|
||||
|
||||
var SignatureAlgorithmDetails = []struct {
|
||||
algo SignatureAlgorithm
|
||||
coseAlg COSEAlgorithmIdentifier
|
||||
name string
|
||||
hasher func() hash.Hash
|
||||
}{
|
||||
{SHA1WithRSA, AlgRS1, "SHA1-RSA", crypto.SHA1.New},
|
||||
{SHA256WithRSA, AlgRS256, "SHA256-RSA", crypto.SHA256.New},
|
||||
{SHA384WithRSA, AlgRS384, "SHA384-RSA", crypto.SHA384.New},
|
||||
{SHA512WithRSA, AlgRS512, "SHA512-RSA", crypto.SHA512.New},
|
||||
{SHA256WithRSAPSS, AlgPS256, "SHA256-RSAPSS", crypto.SHA256.New},
|
||||
{SHA384WithRSAPSS, AlgPS384, "SHA384-RSAPSS", crypto.SHA384.New},
|
||||
{SHA512WithRSAPSS, AlgPS512, "SHA512-RSAPSS", crypto.SHA512.New},
|
||||
{ECDSAWithSHA256, AlgES256, "ECDSA-SHA256", crypto.SHA256.New},
|
||||
{ECDSAWithSHA384, AlgES384, "ECDSA-SHA384", crypto.SHA384.New},
|
||||
{ECDSAWithSHA512, AlgES512, "ECDSA-SHA512", crypto.SHA512.New},
|
||||
{UnknownSignatureAlgorithm, AlgEdDSA, "EdDSA", crypto.SHA512.New},
|
||||
}
|
||||
|
||||
type Error struct {
|
||||
// Short name for the type of error that has occurred
|
||||
Type string `json:"type"`
|
||||
// Additional details about the error
|
||||
Details string `json:"error"`
|
||||
// Information to help debug the error
|
||||
DevInfo string `json:"debug"`
|
||||
}
|
||||
|
||||
var (
|
||||
ErrUnsupportedKey = &Error{
|
||||
Type: "invalid_key_type",
|
||||
Details: "Unsupported Public Key Type",
|
||||
}
|
||||
ErrUnsupportedAlgorithm = &Error{
|
||||
Type: "unsupported_key_algorithm",
|
||||
Details: "Unsupported public key algorithm",
|
||||
}
|
||||
ErrSigNotProvidedOrInvalid = &Error{
|
||||
Type: "signature_not_provided_or_invalid",
|
||||
Details: "Signature invalid or not provided",
|
||||
}
|
||||
)
|
||||
|
||||
func (err *Error) Error() string {
|
||||
return err.Details
|
||||
}
|
||||
|
||||
func (passedError *Error) WithDetails(details string) *Error {
|
||||
err := *passedError
|
||||
err.Details = details
|
||||
return &err
|
||||
}
|
||||
Reference in New Issue
Block a user