@ -1,119 +1,18 @@
|
||||
---
|
||||
Language: Cpp
|
||||
# BasedOnStyle: LLVM
|
||||
AccessModifierOffset: -2
|
||||
BasedOnStyle: Google
|
||||
IndentWidth: 4
|
||||
Language: Cpp
|
||||
ColumnLimit: 100
|
||||
PointerAlignment: Right
|
||||
AlignAfterOpenBracket: Align
|
||||
AlignConsecutiveAssignments: true
|
||||
AlignConsecutiveDeclarations: true
|
||||
#AlignConsecutiveMacros: true
|
||||
AlignEscapedNewlines: Right
|
||||
AlignOperands: true
|
||||
AlignTrailingComments: true
|
||||
#AllowAllArgumentsOnNextLine: false
|
||||
#AllowAllConstructorInitializersOnNextLine: false
|
||||
AlignConsecutiveMacros: true
|
||||
AllowAllParametersOfDeclarationOnNextLine: false
|
||||
AllowShortBlocksOnASingleLine: false
|
||||
SortIncludes: false
|
||||
SpaceAfterCStyleCast: true
|
||||
AllowShortCaseLabelsOnASingleLine: false
|
||||
AllowAllArgumentsOnNextLine: false
|
||||
AllowShortBlocksOnASingleLine: Never
|
||||
AllowShortFunctionsOnASingleLine: None
|
||||
AllowShortIfStatementsOnASingleLine: false
|
||||
#AllowShortLambdasOnASingleLine: None
|
||||
AllowShortLoopsOnASingleLine: false
|
||||
AlwaysBreakAfterDefinitionReturnType: None
|
||||
AlwaysBreakAfterReturnType: None
|
||||
AlwaysBreakBeforeMultilineStrings: false
|
||||
AlwaysBreakTemplateDeclarations: false
|
||||
BinPackArguments: true
|
||||
BinPackArguments: false
|
||||
BinPackParameters: false
|
||||
BraceWrapping:
|
||||
AfterClass: false
|
||||
AfterControlStatement: false
|
||||
AfterEnum: false
|
||||
AfterFunction: false
|
||||
AfterNamespace: false
|
||||
AfterObjCDeclaration: false
|
||||
AfterStruct: false
|
||||
AfterUnion: false
|
||||
AfterExternBlock: false
|
||||
BeforeCatch: false
|
||||
BeforeElse: false
|
||||
IndentBraces: false
|
||||
SplitEmptyFunction: true
|
||||
SplitEmptyRecord: true
|
||||
SplitEmptyNamespace: true
|
||||
BreakBeforeBinaryOperators: None
|
||||
BreakBeforeBraces: Custom
|
||||
BreakBeforeInheritanceComma: false
|
||||
BreakBeforeTernaryOperators: true
|
||||
BreakConstructorInitializersBeforeComma: false
|
||||
BreakConstructorInitializers: BeforeColon
|
||||
BreakAfterJavaFieldAnnotations: false
|
||||
BreakStringLiterals: true
|
||||
ColumnLimit: 120
|
||||
CommentPragmas: '^ IWYU pragma:'
|
||||
CompactNamespaces: false
|
||||
ConstructorInitializerAllOnOneLineOrOnePerLine: false
|
||||
ConstructorInitializerIndentWidth: 4
|
||||
ContinuationIndentWidth: 4
|
||||
Cpp11BracedListStyle: true
|
||||
DerivePointerAlignment: false
|
||||
DisableFormat: false
|
||||
ExperimentalAutoDetectBinPacking: false
|
||||
FixNamespaceComments: true
|
||||
ForEachMacros:
|
||||
- foreach
|
||||
- Q_FOREACH
|
||||
- BOOST_FOREACH
|
||||
IncludeBlocks: Preserve
|
||||
IncludeCategories:
|
||||
- Regex: '^"(llvm|llvm-c|clang|clang-c)/'
|
||||
Priority: 2
|
||||
- Regex: '^(<|"(gtest|gmock|isl|json)/)'
|
||||
Priority: 3
|
||||
- Regex: '.*'
|
||||
Priority: 1
|
||||
IncludeIsMainRegex: '(Test)?$'
|
||||
IndentCaseLabels: false
|
||||
#IndentPPDirectives: BeforeHash
|
||||
IndentWidth: 2
|
||||
IndentWrappedFunctionNames: true
|
||||
JavaScriptQuotes: Leave
|
||||
JavaScriptWrapImports: true
|
||||
KeepEmptyLinesAtTheStartOfBlocks: false
|
||||
MacroBlockBegin: ''
|
||||
MacroBlockEnd: ''
|
||||
MaxEmptyLinesToKeep: 1
|
||||
NamespaceIndentation: All
|
||||
ObjCBlockIndentWidth: 2
|
||||
ObjCSpaceAfterProperty: false
|
||||
ObjCSpaceBeforeProtocolList: true
|
||||
PenaltyBreakAssignment: 2
|
||||
PenaltyBreakBeforeFirstCallParameter: 19
|
||||
PenaltyBreakComment: 300
|
||||
PenaltyBreakFirstLessLess: 120
|
||||
PenaltyBreakString: 1000
|
||||
PenaltyExcessCharacter: 1000000
|
||||
PenaltyReturnTypeOnItsOwnLine: 60
|
||||
PointerAlignment: Right
|
||||
RawStringFormats:
|
||||
- Delimiter: pb
|
||||
Language: TextProto
|
||||
BasedOnStyle: google
|
||||
ReflowComments: true
|
||||
SortIncludes: false
|
||||
SortUsingDeclarations: false
|
||||
SpaceAfterCStyleCast: false
|
||||
SpaceAfterTemplateKeyword: true
|
||||
SpaceBeforeAssignmentOperators: true
|
||||
SpaceBeforeParens: ControlStatements
|
||||
SpaceInEmptyParentheses: false
|
||||
SpacesBeforeTrailingComments: 1
|
||||
SpacesInAngles: false
|
||||
SpacesInContainerLiterals: false
|
||||
SpacesInCStyleCastParentheses: false
|
||||
SpacesInParentheses: false
|
||||
SpacesInSquareBrackets: false
|
||||
Standard: Cpp11
|
||||
TabWidth: 8
|
||||
UseTab: Never
|
||||
...
|
||||
|
||||
---
|
||||
|
@ -0,0 +1,8 @@
|
||||
# Checklist
|
||||
<!-- Put an `x` in each box when you have completed the items. -->
|
||||
- [ ] App update process has been followed <!-- See comment below -->
|
||||
- [ ] Target branch is `develop` <!-- unless you have a very good reason -->
|
||||
- [ ] Application version has been bumped <!-- required if your changes are to be deployed -->
|
||||
|
||||
<!-- Make sure you followed the process described in https://developers.ledger.com/docs/device-app/deliver/maintenance before opening your Pull Request.
|
||||
Don't hesitate to contact us directly on Discord if you have any questions ! https://developers.ledger.com/discord -->
|
@ -0,0 +1,44 @@
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- main
|
||||
- develop
|
||||
pull_request:
|
||||
# Excluded path: add the paths you want to ignore instead of deleting the workflow
|
||||
paths-ignore:
|
||||
- '.github/workflows/*.yml'
|
||||
- 'tests/*'
|
||||
|
||||
jobs:
|
||||
analyse:
|
||||
name: Analyse
|
||||
strategy:
|
||||
matrix:
|
||||
sdk: [ "$NANOS_SDK", "$NANOX_SDK", "$NANOSP_SDK", "$STAX_SDK" ]
|
||||
#'cpp' covers C and C++
|
||||
language: [ 'cpp' ]
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ghcr.io/ledgerhq/ledger-app-builder/ledger-app-builder-legacy:latest
|
||||
|
||||
steps:
|
||||
- name: Clone
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
queries: security-and-quality
|
||||
|
||||
# CodeQL will create the database during the compilation
|
||||
- name: Build
|
||||
run: |
|
||||
make BOLOS_SDK=${{ matrix.sdk }}
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
@ -0,0 +1,25 @@
|
||||
name: Run coding style check through reusable workflow
|
||||
|
||||
# This workflow will run linting checks to ensure a level of uniformization among all Ledger applications.
|
||||
#
|
||||
# The presence of this workflow is mandatory as a minimal level of linting is required.
|
||||
# You are however free to modify the content of the .clang-format file and thus the coding style of your application.
|
||||
# We simply ask you to not diverge too much from the linting of the Boilerplate application.
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- main
|
||||
- develop
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
check_linting:
|
||||
name: Check linting using the reusable workflow
|
||||
uses: LedgerHQ/ledger-app-workflows/.github/workflows/reusable_lint.yml@v1
|
||||
with:
|
||||
source: './src'
|
||||
extensions: 'h,c'
|
||||
version: 11
|
@ -0,0 +1,29 @@
|
||||
name: Misspellings checks
|
||||
|
||||
# This workflow performs some misspelling checks on the repository
|
||||
# It is there to help us maintain a level of quality in our codebase and does not have to be kept on forked
|
||||
# applications.
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- main
|
||||
- develop
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
misspell:
|
||||
name: Check misspellings
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Clone
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Check misspellings
|
||||
uses: codespell-project/actions-codespell@v2
|
||||
with:
|
||||
builtin: clear,rare
|
||||
check_filenames: true
|
||||
ignore_words_list: ontop
|
@ -0,0 +1,43 @@
|
||||
name: Checks on the Python client
|
||||
|
||||
# This workflow performs some checks on the Python client used by the Application tests
|
||||
# It is there to help us maintain a level of quality in our codebase and does not have to be kept on forked
|
||||
# applications.
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- main
|
||||
- develop
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: Client linting
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Clone
|
||||
uses: actions/checkout@v4
|
||||
- name: Installing PIP dependencies
|
||||
run: |
|
||||
pip install pylint
|
||||
pip install -r tests/requirements.txt
|
||||
- name: Lint Python code
|
||||
run: |
|
||||
pylint --rc tests/setup.cfg tests/application_client/
|
||||
|
||||
mypy:
|
||||
name: Type checking
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Clone
|
||||
uses: actions/checkout@v4
|
||||
- name: Installing PIP dependencies
|
||||
run: |
|
||||
pip install mypy
|
||||
pip install -r tests/requirements.txt
|
||||
- name: Mypy type checking
|
||||
run: |
|
||||
mypy tests/application_client/
|
@ -0,0 +1,45 @@
|
||||
name: Checks on the Tools client
|
||||
|
||||
# This workflow performs some checks on the Python client used by the cli tool
|
||||
# It is there to help us maintain a level of quality in our codebase and does not have to be kept on forked
|
||||
# applications.
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- main
|
||||
- develop
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: Client linting
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Clone
|
||||
uses: actions/checkout@v4
|
||||
- name: Installing PIP dependencies
|
||||
run: |
|
||||
sudo apt-get update && sudo apt-get install -y libpcsclite-dev
|
||||
pip install pylint
|
||||
pip install -r pytools/requirements.txt
|
||||
- name: Lint Python code
|
||||
run: |
|
||||
pylint --rc pytools/setup.cfg pytools/
|
||||
|
||||
mypy:
|
||||
name: Type checking
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Clone
|
||||
uses: actions/checkout@v4
|
||||
- name: Installing PIP dependencies
|
||||
run: |
|
||||
sudo apt-get update && sudo apt-get install -y libpcsclite-dev
|
||||
pip install mypy
|
||||
pip install -r pytools/requirements.txt
|
||||
- name: Mypy type checking
|
||||
run: |
|
||||
mypy pytools/
|
@ -0,0 +1,50 @@
|
||||
name: Unit testing with Codecov coverage checking
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- main
|
||||
- develop
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
job_unit_test:
|
||||
name: Unit test
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ghcr.io/ledgerhq/ledger-app-builder/ledger-app-builder-lite:latest
|
||||
|
||||
steps:
|
||||
- name: Clone
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Build unit tests
|
||||
run: |
|
||||
cd unit-tests/
|
||||
cmake -Bbuild -H. && make -C build && make -C build test
|
||||
|
||||
- name: Generate code coverage
|
||||
run: |
|
||||
cd unit-tests/
|
||||
lcov --directory . -b "$(realpath build/)" --capture --initial -o coverage.base && \
|
||||
lcov --rc lcov_branch_coverage=1 --directory . -b "$(realpath build/)" --capture -o coverage.capture && \
|
||||
lcov --directory . -b "$(realpath build/)" --add-tracefile coverage.base --add-tracefile coverage.capture -o coverage.info && \
|
||||
lcov --directory . -b "$(realpath build/)" --remove coverage.info '*/unit-tests/*' -o coverage.info && \
|
||||
genhtml coverage.info -o coverage
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: code-coverage
|
||||
path: unit-tests/coverage
|
||||
|
||||
- name: Upload to codecov.io
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
files: ./unit-tests/coverage.info
|
||||
flags: unittests
|
||||
name: codecov-app-openpgp
|
||||
fail_ci_if_error: true
|
||||
verbose: true
|
@ -1,10 +1,38 @@
|
||||
# Glyphs auto-generated
|
||||
src/glyphs.*
|
||||
|
||||
# Build files
|
||||
dep
|
||||
obj
|
||||
release
|
||||
bin
|
||||
debug
|
||||
build
|
||||
# Compilation files of the application
|
||||
build/
|
||||
|
||||
# Legacy compilation output
|
||||
bin/
|
||||
debug/
|
||||
|
||||
# Temporary directory with snapshots taken during test runs
|
||||
tests/snapshots-tmp/
|
||||
|
||||
# manual-tests
|
||||
manual-tests/foo*
|
||||
manual-tests/gnupg
|
||||
|
||||
# Unit tests and code coverage
|
||||
unit-tests/build/
|
||||
unit-tests/coverage/
|
||||
unit-tests/coverage.info
|
||||
|
||||
# Fuzzing
|
||||
fuzzing/build/
|
||||
|
||||
# Python
|
||||
*.pyc[cod]
|
||||
*.egg
|
||||
__pycache__/
|
||||
*.egg-info/
|
||||
.eggs/
|
||||
.python-version
|
||||
venv/
|
||||
gpg_backup
|
||||
|
||||
# Doxygen
|
||||
doc/html
|
||||
doc/latex
|
||||
|
||||
# Virtual env for sideload (macOS and Windows)
|
||||
ledger/
|
||||
|
@ -0,0 +1,11 @@
|
||||
# Style file for mdl
|
||||
# https://github.com/markdownlint/markdownlint/blob/main/docs/creating_styles.md
|
||||
|
||||
# Include all rules
|
||||
all
|
||||
|
||||
# Disable specific rules
|
||||
#exclude_rule 'MD012'
|
||||
|
||||
# Update rules configuration
|
||||
rule 'MD013', :line_length => 120
|
@ -0,0 +1,11 @@
|
||||
# markdownlint config file
|
||||
|
||||
# Use custom style file
|
||||
style "#{File.dirname(__FILE__)}/.mdl.rb"
|
||||
|
||||
# MD005 - Inconsistent indentation for list items at the same level
|
||||
# MD007 - Unordered list indentation
|
||||
# MD014 - Dollar signs used before commands without showing output
|
||||
# MD024 - Multiple headers with the same content
|
||||
# MD041 - First line in file should be a top level header
|
||||
rules "~MD005,~MD007,~MD014,~MD024,~MD041"
|
@ -0,0 +1,58 @@
|
||||
# To install hooks, run:
|
||||
# pre-commit install --hook-type pre-commit
|
||||
# pre-commit install --hook-type commit-msg
|
||||
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.3.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
- id: end-of-file-fixer
|
||||
- id: mixed-line-ending
|
||||
- id: check-added-large-files
|
||||
- id: check-merge-conflict
|
||||
- id: check-case-conflict
|
||||
|
||||
- repo: https://github.com/codespell-project/codespell
|
||||
rev: v2.2.5
|
||||
hooks:
|
||||
- id: codespell
|
||||
args: ['--ignore-words-list', 'ontop']
|
||||
|
||||
- repo: https://github.com/pre-commit/mirrors-clang-format
|
||||
rev: v11.0.1
|
||||
hooks:
|
||||
- id: clang-format
|
||||
types_or: [c]
|
||||
|
||||
- repo: https://github.com/Mateusz-Grzelinski/actionlint-py
|
||||
rev: v1.6.26.11
|
||||
hooks:
|
||||
- id: actionlint
|
||||
types_or: [yaml]
|
||||
|
||||
- repo: https://github.com/markdownlint/markdownlint
|
||||
rev: v0.13.0
|
||||
hooks:
|
||||
- id: markdownlint
|
||||
types_or: [markdown]
|
||||
|
||||
- repo: local
|
||||
hooks:
|
||||
|
||||
# Python scripts
|
||||
- id: pylint
|
||||
name: Check python Client
|
||||
# Only display messages, no score, disable few errors
|
||||
entry: pylint -j 0 --rc tests/setup.cfg
|
||||
language: system
|
||||
types: [python]
|
||||
files: '^tests\/.*$'
|
||||
|
||||
- id: pylint
|
||||
name: Check python Tool
|
||||
# Only display messages, no score, disable few errors
|
||||
entry: pylint -j 0 --rc pytools/setup.cfg
|
||||
language: system
|
||||
types: [python]
|
||||
files: '^pytools\/.*$'
|
@ -1,232 +1,135 @@
|
||||
#*******************************************************************************
|
||||
# Ledger App
|
||||
# (c) 2016-2019 Ledger
|
||||
# ****************************************************************************
|
||||
# Ledger App OpenPGP
|
||||
# (c) 2024 Ledger SAS.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#*******************************************************************************
|
||||
|
||||
-include Makefile.env
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ****************************************************************************
|
||||
|
||||
ifeq ($(BOLOS_SDK),)
|
||||
$(error Environment variable BOLOS_SDK is not set)
|
||||
endif
|
||||
include $(BOLOS_SDK)/Makefile.defines
|
||||
|
||||
APP_LOAD_PARAMS=--appFlags 0x240 --path "2152157255'" --curve secp256k1 $(COMMON_LOAD_PARAMS)
|
||||
include $(BOLOS_SDK)/Makefile.defines
|
||||
|
||||
ifeq ($(APPNAME),)
|
||||
########################################
|
||||
# Mandatory configuration #
|
||||
########################################
|
||||
# Application name
|
||||
APPNAME = OpenPGP
|
||||
endif
|
||||
ifeq ($(APPNAME),OpenPGP)
|
||||
GPG_MULTISLOT:=0
|
||||
else ifeq ($(APPNAME),OpenPGP.XL)
|
||||
GPG_MULTISLOT:=1
|
||||
APPNAME:=OpenPGP.XL
|
||||
else
|
||||
$(error APPNAME ($(APPNAME)) is not set or unknown)
|
||||
endif
|
||||
|
||||
ifeq ($(TARGET_NAME),TARGET_BLUE)
|
||||
ICONNAME = images/icon_monero_blue.gif
|
||||
else ifeq ($(TARGET_NAME),TARGET_NANOS)
|
||||
ICONNAME = images/icon_pgp.gif
|
||||
else
|
||||
ICONNAME = images/icon_pgp_nanox.gif
|
||||
endif
|
||||
# Application version
|
||||
APPVERSION_M = 2
|
||||
APPVERSION_N = 2
|
||||
APPVERSION_P = 2
|
||||
APPVERSION = "$(APPVERSION_M).$(APPVERSION_N).$(APPVERSION_P)"
|
||||
|
||||
APPVERSION_M:=1
|
||||
APPVERSION_N:=4
|
||||
APPVERSION_P:=4
|
||||
APPVERSION:=$(APPVERSION_M).$(APPVERSION_N).$(APPVERSION_P)
|
||||
SPECVERSION:="3.3.1"
|
||||
|
||||
DEFINES += $(OPENPGP_CONFIG)
|
||||
DEFINES += OPENPGP_VERSION_MAJOR=$(APPVERSION_M) OPENPGP_VERSION_MINOR=$(APPVERSION_N) OPENPGP_VERSION_MICRO=$(APPVERSION_P)
|
||||
DEFINES += OPENPGP_VERSION=$(APPVERSION)
|
||||
DEFINES += OPENPGP_NAME=$(APPNAME)
|
||||
DEFINES += SPEC_VERSION=$(SPECVERSION)
|
||||
DEFINES += GPG_MULTISLOT=$(GPG_MULTISLOT)
|
||||
#DEFINES += GPG_LOG
|
||||
|
||||
ifeq ($(TARGET_NAME),TARGET_BLUE)
|
||||
DEFINES += UI_BLUE
|
||||
else ifeq ($(TARGET_NAME),TARGET_NANOS)
|
||||
DEFINES += UI_NANO_S
|
||||
else
|
||||
DEFINES += UI_NANO_X
|
||||
DEFINES += GPG_SHAKE256
|
||||
endif
|
||||
|
||||
|
||||
|
||||
################
|
||||
# Default rule #
|
||||
################
|
||||
|
||||
.PHONY: allvariants listvariants
|
||||
|
||||
all: default
|
||||
mkdir -p release
|
||||
cp -a bin/app.elf release/$(APPNAME).elf
|
||||
cp -a bin/app.hex release/$(APPNAME).hex
|
||||
cp -a debug/app.asm release/$(APPNAME).asm
|
||||
cp -a debug/app.map release/$(APPNAME).map
|
||||
# Application source files
|
||||
APP_SOURCE_PATH += src
|
||||
APP_SOURCE_FILES += $(BOLOS_SDK)/lib_cxng/src/cx_rsa.c
|
||||
APP_SOURCE_FILES += $(BOLOS_SDK)/lib_cxng/src/cx_pkcs1.c
|
||||
APP_SOURCE_FILES += ${BOLOS_SDK}/lib_cxng/src/cx_ram.c
|
||||
|
||||
INCLUDES_PATH += $(BOLOS_SDK)/lib_cxng/src
|
||||
|
||||
listvariants:
|
||||
@echo VARIANTS APPNAME OpenPGP OpenPGP.XL
|
||||
|
||||
allvariants:
|
||||
make MULTISLOT=0 clean all
|
||||
make MULTISLOT=1 clean all
|
||||
|
||||
############
|
||||
# Platform #
|
||||
############
|
||||
#SCRIPT_LD := script.ld
|
||||
|
||||
ifneq ($(NO_CONSENT),)
|
||||
DEFINES += NO_CONSENT
|
||||
endif
|
||||
# Application icons following guidelines:
|
||||
# https://developers.ledger.com/docs/embedded-app/design-requirements/#device-icon
|
||||
ICON_NANOS = icons/gpg_16px.gif
|
||||
ICON_NANOX = icons/gpg_14px.gif
|
||||
ICON_NANOSP = icons/gpg_14px.gif
|
||||
ICON_STAX = icons/gpg_32px.gif
|
||||
|
||||
# Application allowed derivation curves.
|
||||
# Possibles curves are: secp256k1, secp256r1, ed25519 and bls12381g1
|
||||
# If your app needs it, you can specify multiple curves by using:
|
||||
# `CURVE_APP_LOAD_PARAMS = <curve1> <curve2>`
|
||||
CURVE_APP_LOAD_PARAMS = secp256k1
|
||||
|
||||
# Application allowed derivation paths.
|
||||
# You should request a specific path for your app.
|
||||
# This serve as an isolation mechanism.
|
||||
# Most application will have to request a path according to the BIP-0044
|
||||
# and SLIP-0044 standards.
|
||||
# If your app needs it, you can specify multiple path by using:
|
||||
# `PATH_APP_LOAD_PARAMS = "44'/1'" "45'/1'"`
|
||||
PATH_APP_LOAD_PARAMS = "2152157255'"
|
||||
|
||||
# Setting to allow building variant applications
|
||||
# - <VARIANT_PARAM> is the name of the parameter which should be set
|
||||
# to specify the variant that should be build.
|
||||
# - <VARIANT_VALUES> a list of variant that can be build using this app code.
|
||||
# * It must at least contains one value.
|
||||
# * Values can be the app ticker or anything else but should be unique.
|
||||
VARIANT_PARAM = APPNAME
|
||||
VARIANT_VALUES = OpenPGP
|
||||
|
||||
# Enabling DEBUG flag will enable PRINTF and disable optimizations
|
||||
#DEBUG = 1
|
||||
|
||||
########################################
|
||||
# Application custom permissions #
|
||||
########################################
|
||||
# See SDK `include/appflags.h` for the purpose of each permission
|
||||
#HAVE_APPLICATION_FLAG_DERIVE_MASTER = 1
|
||||
#HAVE_APPLICATION_FLAG_GLOBAL_PIN = 1
|
||||
#HAVE_APPLICATION_FLAG_BOLOS_SETTINGS = 1
|
||||
#HAVE_APPLICATION_FLAG_LIBRARY = 1
|
||||
|
||||
########################################
|
||||
# Application communication interfaces #
|
||||
########################################
|
||||
#ENABLE_BLUETOOTH = 1
|
||||
#ENABLE_NFC = 1
|
||||
|
||||
########################################
|
||||
# NBGL custom features #
|
||||
########################################
|
||||
#ENABLE_NBGL_QRCODE = 1
|
||||
#ENABLE_NBGL_KEYBOARD = 1
|
||||
ENABLE_NBGL_KEYPAD = 1
|
||||
|
||||
########################################
|
||||
# Features disablers #
|
||||
########################################
|
||||
# These advanced settings allow to disable some feature that are by
|
||||
# default enabled in the SDK `Makefile.standard_app`.
|
||||
#DISABLE_STANDARD_APP_FILES = 1
|
||||
#DISABLE_DEFAULT_IO_SEPROXY_BUFFER_SIZE = 1 # To allow custom size declaration
|
||||
#DISABLE_STANDARD_APP_DEFINES = 1 # Will set all the following disablers
|
||||
#DISABLE_STANDARD_SNPRINTF = 1
|
||||
#DISABLE_STANDARD_USB = 1
|
||||
DISABLE_STANDARD_WEBUSB = 1
|
||||
#DISABLE_STANDARD_BAGL_UX_FLOW = 1
|
||||
#DISABLE_DEBUG_LEDGER_ASSERT = 1
|
||||
#DISABLE_DEBUG_THROW = 1
|
||||
|
||||
########################################
|
||||
# Main app configuration #
|
||||
########################################
|
||||
|
||||
DEFINES += OS_IO_SEPROXYHAL
|
||||
DEFINES += HAVE_BAGL HAVE_SPRINTF
|
||||
DEFINES += HAVE_IO_USB HAVE_L4_USBLIB IO_USB_MAX_ENDPOINTS=4 IO_HID_EP_LENGTH=64 HAVE_USB_APDU
|
||||
DEFINES += CUSTOM_IO_APDU_BUFFER_SIZE=\(255+5+64\)
|
||||
DEFINES += HAVE_LEGACY_PID
|
||||
|
||||
DEFINES += USB_SEGMENT_SIZE=64
|
||||
DEFINES += U2F_PROXY_MAGIC=\"MOON\"
|
||||
#DEFINES += HAVE_IO_U2F HAVE_U2F
|
||||
|
||||
DEFINES += UNUSED\(x\)=\(void\)x
|
||||
DEFINES += APPVERSION=\"$(APPVERSION)\"
|
||||
|
||||
DEFINES += HAVE_USB_CLASS_CCID
|
||||
|
||||
ifeq ($(NO_CXNG),)
|
||||
INCLUDES_PATH += $(BOLOS_SDK)/lib_cxng/include
|
||||
endif
|
||||
|
||||
# RSA addition.
|
||||
DEFINES += HAVE_RSA
|
||||
INCLUDES_PATH += $(BOLOS_SDK)/lib_cxng/src
|
||||
SOURCE_PATH += $(BOLOS_SDK)/lib_cxng/src/cx_rsa.c
|
||||
SOURCE_PATH += $(BOLOS_SDK)/lib_cxng/src/cx_pkcs1.c
|
||||
SOURCE_PATH += $(BOLOS_SDK)/lib_cxng/src/cx_utils.c
|
||||
# RSA - End
|
||||
|
||||
ifeq ($(TARGET_NAME),TARGET_NANOX)
|
||||
DEFINES += HAVE_BLE BLE_COMMAND_TIMEOUT_MS=2000
|
||||
DEFINES += HAVE_BLE_APDU # basic ledger apdu transport over BLE
|
||||
endif
|
||||
DEFINES += HAVE_RSA
|
||||
# Watchdog issue causing the device reset with long prime number computation
|
||||
# DEFINES += WITH_SUPPORT_RSA4096
|
||||
|
||||
ifeq ($(TARGET_NAME),TARGET_NANOS)
|
||||
DEFINES += IO_SEPROXYHAL_BUFFER_SIZE_B=128
|
||||
else
|
||||
DEFINES += IO_SEPROXYHAL_BUFFER_SIZE_B=300
|
||||
DEFINES += HAVE_GLO096
|
||||
DEFINES += HAVE_BAGL BAGL_WIDTH=128 BAGL_HEIGHT=64
|
||||
DEFINES += HAVE_BAGL_ELLIPSIS # long label truncation feature
|
||||
DEFINES += HAVE_BAGL_FONT_OPEN_SANS_REGULAR_11PX
|
||||
DEFINES += HAVE_BAGL_FONT_OPEN_SANS_EXTRABOLD_11PX
|
||||
DEFINES += HAVE_BAGL_FONT_OPEN_SANS_LIGHT_16PX
|
||||
DEFINES += HAVE_UX_FLOW
|
||||
endif
|
||||
|
||||
# Enabling debug PRINTF
|
||||
DEBUG = 0
|
||||
ifneq ($(DEBUG),0)
|
||||
|
||||
ifeq ($(TARGET_NAME),TARGET_NANOS)
|
||||
DEFINES += HAVE_PRINTF PRINTF=screen_printf
|
||||
else
|
||||
DEFINES += HAVE_PRINTF PRINTF=mcu_usb_printf
|
||||
endif
|
||||
DEFINES += PLINE="PRINTF(\"FILE:%s..LINE:%d\n\",__FILE__,__LINE__)"
|
||||
else
|
||||
DEFINES += PRINTF\(...\)=
|
||||
DEFINES += PLINE\(...\)=
|
||||
endif
|
||||
|
||||
|
||||
##############
|
||||
# Compiler #
|
||||
##############
|
||||
ifneq ($(BOLOS_ENV),)
|
||||
$(info BOLOS_ENV=$(BOLOS_ENV))
|
||||
CLANGPATH := $(BOLOS_ENV)/clang-arm-fropi/bin/
|
||||
GCCPATH := $(BOLOS_ENV)/gcc-arm-none-eabi-5_3-2016q1/bin/
|
||||
else
|
||||
$(info BOLOS_ENV is not set: falling back to CLANGPATH and GCCPATH)
|
||||
endif
|
||||
ifeq ($(CLANGPATH),)
|
||||
$(info CLANGPATH is not set: clang will be used from PATH)
|
||||
endif
|
||||
ifeq ($(GCCPATH),)
|
||||
$(info GCCPATH is not set: arm-none-eabi-* will be used from PATH)
|
||||
endif
|
||||
CC := $(CLANGPATH)clang
|
||||
|
||||
#CFLAGS += -O0 -gdwarf-2 -gstrict-dwarf
|
||||
CFLAGS += -O3 -Os
|
||||
#CFLAGS += -fno-jump-tables -fno-lookup-tables -fsave-optimization-record
|
||||
#$(info $(CFLAGS))
|
||||
|
||||
AS := $(GCCPATH)arm-none-eabi-gcc
|
||||
|
||||
LD := $(GCCPATH)arm-none-eabi-gcc
|
||||
#LDFLAGS += -O0 -gdwarf-2 -gstrict-dwarf
|
||||
LDFLAGS += -O3 -Os
|
||||
LDLIBS += -lm -lgcc -lc
|
||||
|
||||
# import rules to compile glyphs(/pone)
|
||||
include $(BOLOS_SDK)/Makefile.glyphs
|
||||
|
||||
### variables processed by the common makefile.rules of the SDK to grab source files and include dirs
|
||||
APP_SOURCE_PATH += src
|
||||
SDK_SOURCE_PATH += lib_stusb lib_stusb_impl
|
||||
SDK_SOURCE_PATH += lib_ux
|
||||
ifeq ($(TARGET_NAME),TARGET_NANOX)
|
||||
SDK_SOURCE_PATH += lib_blewbxx lib_blewbxx_impl
|
||||
DEFINES += HAVE_UX_LEGACY
|
||||
endif
|
||||
|
||||
#########################
|
||||
|
||||
cformat:
|
||||
clang-format -i src/*.c src/*.h
|
||||
|
||||
load: all
|
||||
cp -a release/$(APPNAME).elf bin/app.elf
|
||||
cp -a release/$(APPNAME).hex bin/app.hex
|
||||
cp -a release/$(APPNAME).asm debug/app.asm
|
||||
cp -a release/$(APPNAME).map debug/app.map
|
||||
python -m ledgerblue.loadApp $(APP_LOAD_PARAMS)
|
||||
|
||||
run:
|
||||
python -m ledgerblue.runApp --appName $(APPNAME)
|
||||
|
||||
exit:
|
||||
echo -e "0020008206313233343536\n0002000000" |scriptor -r "Ledger Nano S [Nano S] (0001) 01 00"
|
||||
|
||||
delete:
|
||||
python -m ledgerblue.deleteApp $(COMMON_DELETE_PARAMS)
|
||||
|
||||
|
||||
|
||||
# import generic rules from the user and SDK
|
||||
-include Makefile.rules
|
||||
#include $(BOLOS_SDK)/Makefile.rules
|
||||
|
||||
#add dependency on custom makefile filename
|
||||
dep/%.d: %.c Makefile
|
||||
|
||||
|
||||
# Import generic rules from the SDK
|
||||
include $(BOLOS_SDK)/Makefile.standard_app
|
||||
|
@ -1,39 +0,0 @@
|
||||
#*******************************************************************************
|
||||
# Ledger SDK
|
||||
# (c) 2017 Ledger
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#*******************************************************************************
|
||||
|
||||
# temporary redef, to ensure wider compliance of the SDK with pre-1.6 apps
|
||||
ifeq ($(TARGET_NAME),$(filter $(TARGET_NAME),TARGET_NANOX TARGET_NANOS2))
|
||||
SDK_SOURCE_PATH += lib_bagl
|
||||
else
|
||||
SDK_SOURCE_PATH += lib_bagl lib_ux
|
||||
endif
|
||||
|
||||
# adding the correct target header to sources
|
||||
SDK_SOURCE_PATH += target/$(TARGET)/include
|
||||
|
||||
# Expose all SDK header files with their full relative path to the SDK root folder
|
||||
INCLUDES_PATH += ${BOLOS_SDK}
|
||||
|
||||
SOURCE_PATH += $(dir $(foreach libdir, $(APP_SOURCE_PATH), $(dir $(shell find $(libdir) -name '*.[csS]')))) $(BOLOS_SDK)/src $(foreach libdir, $(SDK_SOURCE_PATH), $(dir $(shell find $(BOLOS_SDK)/$(libdir) -name '*.[csS]')))
|
||||
SOURCE_FILES := $(foreach path, $(SOURCE_PATH),$(shell find $(path) -name '*.[csS]') ) $(GLYPH_DESTC)
|
||||
INCLUDES_PATH += $(dir $(foreach libdir, $(SDK_SOURCE_PATH), $(dir $(shell find $(BOLOS_SDK)/$(libdir) -name '*.h')))) include $(BOLOS_SDK)/include $(BOLOS_SDK)/include/arm $(dir $(shell find $(APP_SOURCE_PATH) -name '*.h')) $(GLYPH_SRC_DIR)
|
||||
|
||||
VPATH += $(dir $(SOURCE_FILES))
|
||||
OBJECT_FILES += $(sort $(addprefix $(OBJ_DIR)/, $(addsuffix .o, $(basename $(notdir $(SOURCE_FILES))))))
|
||||
DEPEND_FILES += $(sort $(addprefix $(DEP_DIR)/, $(addsuffix .d, $(basename $(notdir $(SOURCE_FILES))))))
|
||||
|
||||
include $(BOLOS_SDK)/Makefile.rules_generic
|
@ -1,59 +1,331 @@
|
||||
## GnuPG application: blue-app-gnupg
|
||||
# GnuPG application
|
||||
|
||||
GnuPG application for Nano S and Nano X
|
||||
[![Ensure compliance with Ledger guidelines](https://github.com/LedgerHQ/app-openpgp/actions/workflows/guidelines_enforcer.yml/badge.svg)](https://github.com/LedgerHQ/app-openpgp/actions/workflows/guidelines_enforcer.yml)
|
||||
|
||||
This application implements "The OpenPGP card" specification revision 3.0. This specification is available in *doc* directory and at https://g10code.com/p-card.html .
|
||||
|
||||
The application supports:
|
||||
- RSA with key up to 4096 bits
|
||||
- ECDSA with secp256k1
|
||||
- EDDSA with Ed25519 curve
|
||||
- ECDH with secp256k1 and curve25519 curves
|
||||
[![Build and run functional tests using ragger through reusable workflow](https://github.com/LedgerHQ/app-openpgp/actions/workflows/build_and_functional_tests.yml/badge.svg?branch=master)](https://github.com/LedgerHQ/app-openpgp/actions/workflows/build_and_functional_tests.yml)
|
||||
|
||||
GnuPG application for Ledger devices
|
||||
|
||||
This release has known missing parts (see also Add-on) :
|
||||
This application implements "The OpenPGP card" specification revision 3.3.
|
||||
This specification is available in *doc* directory and at <https://g10code.com/p-card.html>.
|
||||
|
||||
* Ledger Blue support
|
||||
* Seed mode ON/OFF via apdu
|
||||
The application supports:
|
||||
|
||||
- RSA with key up to 3072 bits
|
||||
- ECDSA with secp256R1 and secp256K1
|
||||
- EDDSA with Ed25519 curve
|
||||
- ECDH with secp256R1, secp256K1 and curve25519 curves
|
||||
|
||||
## Installation and Usage
|
||||
|
||||
See the full doc at https://github.com/LedgerHQ/blue-app-openpgp-card/blob/master/doc/user/blue-app-openpgp-card.pdf
|
||||
|
||||
See the full doc in [rst](doc/user/app-openpgp.rst), or in [pdf](<https://github.com/LedgerHQ/app-openpgp/blob/master/doc/user/app-openpgp.pdf>)
|
||||
|
||||
## Add-on
|
||||
|
||||
The GnuPG application implements the following addon:
|
||||
- serial modification
|
||||
- on screen reset
|
||||
- 3 independent key slots
|
||||
- seeded key generation
|
||||
|
||||
Technical specification is available at https://github.com/LedgerHQ/blue-app-openpgp-card/blob/master/doc/developper/gpgcard3.0-addon.rst
|
||||
- Serial modification
|
||||
- On screen reset
|
||||
- 3 independent key slots (except for Nanos, where we have only a single slot)
|
||||
- Seeded key generation
|
||||
|
||||
Technical specification is available in [rst](doc/developer/gpgcard-addon.rst), or in [pdf](<https://github.com/LedgerHQ/app-openpgp/blob/master/doc/developer/gpgcard-addon.pdf>)
|
||||
|
||||
|
||||
### Key slot
|
||||
|
||||
"The OpenPGP card" specification specifies:
|
||||
- 3 asymmetric keys : Signature, Decryption, Authentication
|
||||
- 1 symmetric key
|
||||
The OpenPGP card specification indicates:
|
||||
|
||||
- 3 asymmetric keys:
|
||||
- Signature,
|
||||
- Decryption
|
||||
- Authentication
|
||||
- 1 symmetric key
|
||||
|
||||
The blue application allow you to store 3 different key sets, named slot. Each slot contains the above 4 keys.
|
||||
The application allows you to store 3 different key sets, named slot. Each slot contains the above 4 keys.
|
||||
You can choose the active slot on the main screen.
|
||||
When installed the default slot is "1". You can change it in settings.
|
||||
|
||||
|
||||
### seeded key generation
|
||||
### Seeded key generation
|
||||
|
||||
A seeded mode is implemented in order to restore private keys on a new token.
|
||||
In this mode key material is generated from the global token seeded.
|
||||
In this mode, key material is generated from the global token seed.
|
||||
|
||||
Please consider SEED mode as experimental.
|
||||
Also, a backup/restore mechanism is provided. Please report to the [Documentation](#documentation).
|
||||
|
||||
More details to come...
|
||||
> Warning: Without such configuration, an OS or App update will cause your private key to be lost!"
|
||||
|
||||
The following is a repeatable process that will generate the same keys and fingerprints
|
||||
(even with different card serial numbers).
|
||||
|
||||
1. Reset the OpenPGP App
|
||||
1. Set Key Templates from the OpenPGP App menu, if needed
|
||||
1. On computer, use `gpg` to edit the key with a fixed timestamp:
|
||||
|
||||
```shell
|
||||
gpg --faked-system-time 19990101T000000! --card-edit # note the exclamation mark to keep the time fixed
|
||||
gpg> admin
|
||||
gpg> generate
|
||||
... # complete the wizard
|
||||
```
|
||||
|
||||
While doing this, ensure that you use the same `--faked-system-time` and "Real Name" and "Email"
|
||||
during the generation wizard to reproduce the same keys each time.
|
||||
|
||||
### On screen reset
|
||||
|
||||
The application can be reset as if it was fresh installed. In settings, choose reset and confirm.
|
||||
|
||||
## Quick start guide
|
||||
|
||||
### With VSCode
|
||||
|
||||
You can quickly setup a convenient environment to build and test your application by using
|
||||
[Ledger's VSCode developer tools extension](https://marketplace.visualstudio.com/items?itemName=LedgerHQ.ledger-dev-tools)
|
||||
which leverages the [ledger-app-dev-tools](https://github.com/LedgerHQ/ledger-app-builder/pkgs/container/ledger-app-builder%2Fledger-app-dev-tools)
|
||||
docker image.
|
||||
|
||||
It will allow you, whether you are developing on macOS, Windows or Linux,
|
||||
to quickly **build** your apps, **test** them on **Speculos** and **load** them on any supported device.
|
||||
|
||||
- Install and run [Docker](https://www.docker.com/products/docker-desktop/).
|
||||
- Make sure you have an X11 server running:
|
||||
- On Ubuntu Linux, it should be running by default.
|
||||
- On macOS, install and launch [XQuartz](https://www.xquartz.org/)
|
||||
(make sure to go to XQuartz > Preferences > Security and check "Allow client connections").
|
||||
- On Windows, install and launch [VcXsrv](https://sourceforge.net/projects/vcxsrv/)
|
||||
(make sure to configure it to disable access control).
|
||||
- Install [VScode](https://code.visualstudio.com/download) and add [Ledger's extension](https://marketplace.visualstudio.com/items?itemName=LedgerHQ.ledger-dev-tools).
|
||||
- Open a terminal and clone `app-openpgp` with `git clone git@github.com:LedgerHQ/app-openpgp.git`.
|
||||
- Open the `app-openpgp` folder with VSCode.
|
||||
- Use Ledger extension's sidebar menu or open the tasks menu with `ctrl + shift + b`
|
||||
(`command + shift + b` on a Mac) to conveniently execute actions:
|
||||
- Build the app for the device model of your choice with `Build`.
|
||||
- Test your binary on [Speculos](https://github.com/LedgerHQ/speculos) with `Run with Speculos`.
|
||||
- You can also run functional tests, load the app on a physical device, and more.
|
||||
|
||||
> The terminal tab of VSCode will show you what commands the extension runs behind the scene.
|
||||
|
||||
### With a terminal
|
||||
|
||||
The [ledger-app-dev-tools](https://github.com/LedgerHQ/ledger-app-builder/pkgs/container/ledger-app-builder%2Fledger-app-dev-tools)
|
||||
docker image contains all the required tools and libraries to **build**, **test** and **load** an application.
|
||||
|
||||
You can download it from the ghcr.io docker repository:
|
||||
|
||||
```shell
|
||||
sudo docker pull ghcr.io/ledgerhq/ledger-app-builder/ledger-app-dev-tools:latest
|
||||
```
|
||||
|
||||
You can then enter this development environment by executing the following command
|
||||
from the directory of the application `git` repository:
|
||||
|
||||
#### Linux (Ubuntu)
|
||||
|
||||
```shell
|
||||
sudo docker run --rm -ti --user "$(id -u):$(id -g)" --privileged -v "/dev/bus/usb:/dev/bus/usb" -v "$(realpath .):/app" ghcr.io/ledgerhq/ledger-app-builder/ledger-app-dev-tools:latest
|
||||
```
|
||||
|
||||
#### macOS
|
||||
|
||||
```shell
|
||||
sudo docker run --rm -ti --user "$(id -u):$(id -g)" --privileged -v "$(pwd -P):/app" ghcr.io/ledgerhq/ledger-app-builder/ledger-app-dev-tools:latest
|
||||
```
|
||||
|
||||
#### Windows (with PowerShell)
|
||||
|
||||
```shell
|
||||
docker run --rm -ti --privileged -v "$(Get-Location):/app" ghcr.io/ledgerhq/ledger-app-builder/ledger-app-dev-tools:latest
|
||||
```
|
||||
|
||||
The application's code will be available from inside the docker container,
|
||||
you can proceed to the following compilation steps to build your app.
|
||||
|
||||
## Compilation and load
|
||||
|
||||
To easily setup a development environment for compilation and loading on a physical device, you can use the [VSCode integration](#with-vscode)
|
||||
whether you are on Linux, macOS or Windows.
|
||||
|
||||
If you prefer using a terminal to perform the steps manually, you can use the guide below.
|
||||
|
||||
### Compilation
|
||||
|
||||
Setup a compilation environment by following the [shell with docker approach](#with-a-terminal).
|
||||
|
||||
From inside the container, use the following command to build the app:
|
||||
|
||||
```shell
|
||||
make DEBUG=1 # compile optionally with PRINTF
|
||||
```
|
||||
|
||||
You can choose which device to compile and load for by setting the `BOLOS_SDK` environment variable to the following values:
|
||||
|
||||
- `BOLOS_SDK=$NANOS_SDK`
|
||||
- `BOLOS_SDK=$NANOX_SDK`
|
||||
- `BOLOS_SDK=$NANOSP_SDK`
|
||||
- `BOLOS_SDK=$STAX_SDK`
|
||||
|
||||
### Loading on a physical device
|
||||
|
||||
This step will vary slightly depending on your platform.
|
||||
|
||||
> Your physical device must be connected, unlocked and the screen showing the dashboard (not inside an application).
|
||||
|
||||
#### Linux (Ubuntu)
|
||||
|
||||
First make sure you have the proper udev rules added on your host.
|
||||
See [udev-rules](https://github.com/LedgerHQ/udev-rules)
|
||||
|
||||
Then once you have [opened a terminal](#with-a-terminal) in the `app-builder` image and [built the app](#compilation-and-load)
|
||||
for the device you want, run the following command:
|
||||
|
||||
```shell
|
||||
# Run this command from the app-builder container terminal.
|
||||
make load # load the app on a Nano S by default
|
||||
```
|
||||
|
||||
[Setting the BOLOS_SDK environment variable](#compilation-and-load) will allow you to load
|
||||
on whichever supported device you want.
|
||||
|
||||
#### macOS / Windows (with PowerShell)
|
||||
|
||||
> It is assumed you have [Python](https://www.python.org/downloads/) installed on your computer.
|
||||
|
||||
Run these commands on your host from the app's source folder once you have [built the app](#compilation-and-load)
|
||||
for the device you want:
|
||||
|
||||
```shell
|
||||
# Install Python virtualenv
|
||||
python3 -m pip install virtualenv
|
||||
# Create the 'ledger' virtualenv
|
||||
python3 -m virtualenv ledger
|
||||
```
|
||||
|
||||
Enter the Python virtual environment
|
||||
|
||||
- macOS: `source ledger/bin/activate`
|
||||
- Windows: `.\ledger\Scripts\Activate.ps1`
|
||||
|
||||
```shell
|
||||
# Install Ledgerblue (tool to load the app)
|
||||
python3 -m pip install ledgerblue
|
||||
# Load the app.
|
||||
python3 -m ledgerblue.runScript --scp --fileName bin/app.apdu --elfFile bin/app.elf
|
||||
```
|
||||
|
||||
## Tests
|
||||
|
||||
The OpenPGP app comes with different tests:
|
||||
|
||||
- Functional Tests implemented with Ledger's [Ragger](https://github.com/LedgerHQ/ragger) test framework.
|
||||
- Unit Tests, allowing to test basic simple functions
|
||||
- Manual Tests, using some script to perform real tests on the device:
|
||||
- Key generation
|
||||
- Encryption/Decryption
|
||||
- Sign/Verify
|
||||
|
||||
### Functional Tests (Ragger based)
|
||||
|
||||
#### Linux (Ubuntu)
|
||||
|
||||
On Linux, you can use [Ledger's VS Code extension](#with-vscode) to run the tests.
|
||||
If you prefer not to, open a terminal and follow the steps below.
|
||||
|
||||
Install the tests requirements:
|
||||
|
||||
```shell
|
||||
pip install -r tests/requirements.txt
|
||||
```
|
||||
|
||||
Then you can:
|
||||
|
||||
Run the functional tests (here for nanos but available for any device once you have built the binaries):
|
||||
|
||||
```shell
|
||||
pytest tests/ --tb=short -v --device nanos
|
||||
```
|
||||
|
||||
Or run your app directly with Speculos
|
||||
|
||||
```shell
|
||||
speculos --model nanos build/nanos/bin/app.elf
|
||||
```
|
||||
|
||||
#### macOS / Windows
|
||||
|
||||
To test your app on macOS or Windows, it is recommended to use [Ledger's VS Code extension](#with-vscode)
|
||||
to quickly setup a working test environment.
|
||||
|
||||
You can use the following sequence of tasks and commands (all accessible in the **extension sidebar menu**):
|
||||
|
||||
- `Select build target`
|
||||
- `Build app`
|
||||
|
||||
Then you can choose to execute the functional tests:
|
||||
|
||||
- Use `Run tests`.
|
||||
|
||||
Or simply run the app on the Speculos emulator:
|
||||
|
||||
- `Run with Speculos`.
|
||||
|
||||
### Unit Tests
|
||||
|
||||
Those tests are available in the directory `unit-tests`. Please see the corresponding [README](unit-tests/README.md)
|
||||
to compile and run them.
|
||||
|
||||
### Manual Tests
|
||||
|
||||
Those tests are available in the directory `manual-tests`. This consists in a helper script (`manual.sh`)
|
||||
corresponding to different cases described in the document [Quick tests](doc/developer/quick-test.md).
|
||||
|
||||
## Documentation
|
||||
|
||||
Several documents are available.
|
||||
|
||||
Functional Specification of the OpenPGP Application available [here](<https://github.com/LedgerHQ/app-openpgp/blob/master/doc/specification/OpenPGP-smart-card-application.pdf>),
|
||||
but also at <https://g10code.com/p-card.html>.
|
||||
|
||||
User documentation for the Ledger Application available here: [rst](doc/user/app-openpgp.rst),
|
||||
or [pdf](<https://github.com/LedgerHQ/app-openpgp/blob/master/doc/user/app-openpgp.pdf>)
|
||||
|
||||
Developer documentation related to the Ledger Add-ons available here: [rst](doc/developer/gpgcard-addon.rst),
|
||||
or [pdf](<https://github.com/LedgerHQ/app-openpgp/blob/master/doc/developer/gpgcard-addon.pdf>)
|
||||
|
||||
A Quick Test document to perform `Manual Tests` available [here](doc/developer/quick-test.md)
|
||||
|
||||
The pdf documentation for **User** and **Developer** are available, and can be generated from
|
||||
the corresponding `.rst` files (in the same respective directories) using this command:
|
||||
|
||||
```shell
|
||||
cd doc/
|
||||
./generate.sh
|
||||
```
|
||||
|
||||
## Continuous Integration
|
||||
|
||||
The flow processed in [GitHub Actions](https://github.com/features/actions) is the following:
|
||||
|
||||
- Ledger guidelines enforcer which verifies that an app is compliant with Ledger guidelines.
|
||||
The successful completion of this reusable workflow is a mandatory step for an app to be available
|
||||
on the Ledger application store. More information on the guidelines can be found
|
||||
in the repository [ledger-app-workflow](https://github.com/LedgerHQ/ledger-app-workflows)
|
||||
- Code formatting with [clang-format](http://clang.llvm.org/docs/ClangFormat.html)
|
||||
- Compilation of the application for all Ledger hardware in [ledger-app-builder](https://github.com/LedgerHQ/ledger-app-builder)
|
||||
- Unit tests of C functions with [cmocka](https://cmocka.org/) (see [unit-tests/](unit-tests/))
|
||||
- End-to-end tests with [Speculos](https://github.com/LedgerHQ/speculos) emulator
|
||||
and [ragger](https://github.com/LedgerHQ/ragger) (see [tests/](tests/))
|
||||
- Code coverage with [gcov](https://gcc.gnu.org/onlinedocs/gcc/Gcov.html)/[lcov](http://ltp.sourceforge.net/coverage/lcov.php)
|
||||
and upload to [codecov.io](https://about.codecov.io)
|
||||
|
||||
It outputs 3 artifacts:
|
||||
|
||||
- `compiled_app_binaries` within binary files of the build process for each device
|
||||
- `code-coverage` within HTML details of code coverage
|
||||
|
||||
## Known limitations
|
||||
|
||||
Today, the current App has some known limitations.
|
||||
|
||||
- RSA4096 is disabled, because of an issue with the watchdog, resetting the device
|
||||
during long prime number operation.
|
||||
- Using Ed25519 template, the decrypt doesn't output a correct result.
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 17 KiB |
@ -0,0 +1,344 @@
|
||||
..
|
||||
Ledger App OpenPGP.
|
||||
(c) 2024 Ledger SAS.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
..
|
||||
------------------------------------------------------------------------
|
||||
LaTex substitution Definition
|
||||
------------------------------------------------------------------------
|
||||
|
||||
|
||||
|
||||
License
|
||||
=======
|
||||
|
||||
| Ledger App OpenPGP.
|
||||
| (c) 2024 Ledger SAS.
|
||||
|
|
||||
| Licensed under the Apache License, Version 2.0 (the "License");
|
||||
| you may not use this file except in compliance with the License.
|
||||
| You may obtain a copy of the License at
|
||||
|
|
||||
| http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
|
||||
| Unless required by applicable law or agreed to in writing, software
|
||||
| distributed under the License is distributed on an "AS IS" BASIS,
|
||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
| See the License for the specific language governing permissions and
|
||||
| limitations under the License.
|
||||
|
||||
|
||||
Introduction
|
||||
============
|
||||
|
||||
OpenPGP Card Application add-ons summary
|
||||
----------------------------------------
|
||||
|
||||
Key management:
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
OpenPGP Application manage 4 keys to Perform Security Operation (PSO) plus 2 for secure channel.
|
||||
|
||||
The 4 keys are defined as follow:
|
||||
|
||||
- One asymmetric signature private key (RSA or EC), named 'sig'
|
||||
- One asymmetric decryption private key (RSA or EC), named 'dec'
|
||||
- One asymmetric authentication private key (RSA or EC), named 'aut'
|
||||
- One symmetric decryption private key (AES), named 'sym0'
|
||||
|
||||
The 3 first asymmetric keys can be either randomly generated on-card or
|
||||
explicitly imported from outside.
|
||||
|
||||
The 4th is imported from outside.
|
||||
|
||||
It's never possible to retrieve private key from the card.
|
||||
|
||||
This add-on specification propose a solution to derive those keys from the
|
||||
master seed managed by the Ledger Token.
|
||||
This allows owner to restore a broken token without the needs to keep track of keys
|
||||
outside the card.
|
||||
|
||||
Moreover this add-on specification propose to manage multiple set of the 4 previously described keys.
|
||||
|
||||
Keys Slots
|
||||
~~~~~~~~~~
|
||||
|
||||
To modify the keys slot, just select the corresponding menu from the screen.
|
||||
|
||||
.. image:: slots.png
|
||||
|
||||
Random number generation
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
OpenPGP Application provides, as optional feature, to generate random bytes.
|
||||
|
||||
This add-on specification propose new type of random generation:
|
||||
|
||||
- random prime number generation
|
||||
- seeded random number
|
||||
- seeded prime number generation
|
||||
|
||||
Key Backup
|
||||
~~~~~~~~~~
|
||||
|
||||
A full key backup mechanism is provided.
|
||||
|
||||
|
||||
Ledger OpenPGP Application
|
||||
==========================
|
||||
|
||||
How
|
||||
---
|
||||
|
||||
Deterministic key derivation
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The deterministic key derivation process relies on the **BIP32** scheme.
|
||||
The master install path of the App is set to ``/0x80'GPG'``, aka ``/80475047``.
|
||||
|
||||
Deterministic key derivation maybe activated in:
|
||||
|
||||
| ``Settings -> Seed Mode -> Set on``
|
||||
|
||||
This activation remains effective until *set off* is selected.
|
||||
|
||||
The key management remains the same if seed mode is *on* or *off*. So there is no performance impact when using seeded keys.
|
||||
|
||||
Seeded keys are generated as follow:
|
||||
|
||||
**Step 1**:
|
||||
|
||||
For a given keys slot n, starting from 1, a seed is first derived with the following path
|
||||
|
||||
| ``Sn = BIP32_derive (/0x80475047/n)``
|
||||
|
||||
**Step 2**:
|
||||
|
||||
Then specific seeds are derived with the *SHA3-XOF* function for each of the 4 key:
|
||||
|
||||
| ``Sk[i] = SHA3-XOF(SHA256(Sn \| <key_name> \| int16(i)), length)``
|
||||
|
||||
Where:
|
||||
|
||||
- Sn is the dedicated slot seed from step 1.
|
||||
- key_name is one of 'sig ','dec ', 'aut ', 'sym0', each 4 characters.
|
||||
- i is the index, starting from 1, of the desired seed (see below)
|
||||
|
||||
**Step 3**:
|
||||
|
||||
*RSA key generation*
|
||||
|
||||
Generate two seed Sp, Sq in step2 with:
|
||||
|
||||
- i € {1,2}
|
||||
- length equals to half key size
|
||||
|
||||
Generate two prime numbers p, q:
|
||||
|
||||
- p = next_prime(Sp)
|
||||
- q = next_prime(Sq)
|
||||
|
||||
Generate RSA key pair as usual:
|
||||
|
||||
- choose e
|
||||
- n = p*q
|
||||
- d = inv(e) mod (p-1)(q-1)
|
||||
|
||||
*ECC key generation*
|
||||
|
||||
Generate one seed Sd in step2 with:
|
||||
|
||||
- i = 1
|
||||
- length equals to curve size
|
||||
|
||||
Generate ECC key pair:
|
||||
|
||||
- d = Sd
|
||||
- W = d.G
|
||||
|
||||
*AES key generation*
|
||||
|
||||
Generate one seed Sd in step2 with:
|
||||
|
||||
- i = 1
|
||||
- length equals to 16
|
||||
|
||||
Generate AES key:
|
||||
|
||||
- k = Sk
|
||||
|
||||
Deterministic random number
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The deterministic random number generation relies on the **BIP32** scheme.
|
||||
The master install path of the App is set to ``/0x80'GPG'``, aka ``/80475047``.
|
||||
|
||||
**Random prime number generation**:
|
||||
|
||||
For a given length *L*:
|
||||
|
||||
- generate random number r of *L* bytes.
|
||||
- generate rp = next_prime(r)
|
||||
- return rp
|
||||
|
||||
**Seeded random number**:
|
||||
|
||||
For a given length *L* and seed *S*:
|
||||
|
||||
- generate Sr = BIP32_derive(/0x80475047/0x0F0F0F0F)
|
||||
- generate r = SHA3-XOF(SHA256(Sr \| 'rnd' \| S), L)
|
||||
- return r
|
||||
|
||||
**Seeded prime number generation**:
|
||||
|
||||
For a given length *L* and seed *S*:
|
||||
|
||||
- generate r as for "Seeded random number"
|
||||
- generate rp = next_prime(r)
|
||||
- return rp
|
||||
|
||||
Key Backup & Restore
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
In order to backup/restore private key the commands `put_data` and `get_data` accept the tags:
|
||||
|
||||
- `B6` (signature key)
|
||||
- `B8` (encryption key)
|
||||
- `A4` (authentication).
|
||||
|
||||
`put_data` command accept the exact output of `get_data`. The `get_data` command
|
||||
return both the public and private key.
|
||||
|
||||
For security and confidentiality, private key is returned encrypted in AES.
|
||||
The key used is derived according to previously described AES key derivation
|
||||
with name 'key'.
|
||||
|
||||
|
||||
The data payload is formatted as follow:
|
||||
|
||||
+-------+--------------------------+
|
||||
| size | Description |
|
||||
+=======+==========================+
|
||||
| 4 | OS Target ID |
|
||||
+-------+--------------------------+
|
||||
| 4 | API Level |
|
||||
+-------+--------------------------+
|
||||
| 4 | compliance Level |
|
||||
+-------+--------------------------+
|
||||
| 4 | public key size |
|
||||
+-------+--------------------------+
|
||||
| var | public key |
|
||||
+-------+--------------------------+
|
||||
| 4 | private key size |
|
||||
+-------+--------------------------+
|
||||
| var | encrypted private key |
|
||||
+-------+--------------------------+
|
||||
|
||||
APDU Modification
|
||||
-----------------
|
||||
|
||||
Key Slot management
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Key slots are managed by data object *01F1* and *01F2* witch are
|
||||
manageable by PUT/GET DATA command as for others DO and organized as follow.
|
||||
|
||||
On application reset, the *01F2* content is set to *Default Slot* value
|
||||
of *01F1*.
|
||||
|
||||
*01F1:*
|
||||
|
||||
+-------+----------------------------------+-------+
|
||||
| bytes | Description | R/W |
|
||||
+=======+==================================+=======+
|
||||
| 1 | Number of slot | R |
|
||||
+-------+----------------------------------+-------+
|
||||
| 2 | Default slot | R/W |
|
||||
+-------+----------------------------------+-------+
|
||||
| 3 | Allowed slot selection method | R/W |
|
||||
+-------+----------------------------------+-------+
|
||||
|
||||
Byte 3 is endoced as follow:
|
||||
|
||||
+----+----+----+----+----+----+----+----+-------------------------+
|
||||
| b8 | b7 | b6 | b5 | b4 | b3 | b2 | b1 | Meaning |
|
||||
+----+----+----+----+----+----+----+----+-------------------------+
|
||||
| \- | \- | \- | \- | \- | \- | \- | x | selection by APDU |
|
||||
+----+----+----+----+----+----+----+----+-------------------------+
|
||||
| \- | \- | \- | \- | \- | \- | x | \- | selection by screen |
|
||||
+----+----+----+----+----+----+----+----+-------------------------+
|
||||
|
||||
*01F2:*
|
||||
|
||||
+--------+-----------------+-------+
|
||||
| bytes | Description | R/W |
|
||||
+========+=================+=======+
|
||||
| 1 | Current slot | R/W |
|
||||
+--------+-----------------+-------+
|
||||
|
||||
*01F0:*
|
||||
|
||||
+--------+-----------------+-------+
|
||||
| bytes | Description | R/W |
|
||||
+========+=================+=======+
|
||||
| 1-3 | 01F1 content | R |
|
||||
+--------+-----------------+-------+
|
||||
| 4 | 01F2 content | R |
|
||||
+--------+-----------------+-------+
|
||||
|
||||
*Access Conditions:*
|
||||
|
||||
+------+-----------+------------+
|
||||
| DO | Read | Write |
|
||||
+======+===========+============+
|
||||
| 01F0 | Always | Never |
|
||||
+------+-----------+------------+
|
||||
| 01F1 | Always | Verify PW3 |
|
||||
+------+-----------+------------+
|
||||
| 01F2 | Always | Verify PW2 |
|
||||
+------+-----------+------------+
|
||||
|
||||
Deterministic key derivation
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
P2 parameter of GENERATE ASYMMETRIC KEY PAIR is set to (hex value):
|
||||
|
||||
- 00 for true random key generation
|
||||
- 01 for seeded random key
|
||||
|
||||
Deterministic random number
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
P1 parameter of GET CHALLENGE is a bit-field encoded as follow:
|
||||
|
||||
+----+----+----+----+----+----+----+----+-------------------------+
|
||||
| b8 | b7 | b6 | b5 | b4 | b3 | b2 | b1 | Meaning |
|
||||
+----+----+----+----+----+----+----+----+-------------------------+
|
||||
| \- | \- | \- | \- | \- | \- | \- | x | seeded random |
|
||||
+----+----+----+----+----+----+----+----+-------------------------+
|
||||
| \- | \- | \- | \- | \- | \- | x | \- | prime random |
|
||||
+----+----+----+----+----+----+----+----+-------------------------+
|
||||
|
||||
When *seeded mode* is set, data field contains the seed and P2 contains
|
||||
the length of random bytes to generate.
|
||||
|
||||
|
||||
Other minor add-on
|
||||
------------------
|
||||
|
||||
GnuPG use both fingerprints and serial number to identify key on card.
|
||||
So, the `put_data` command is able to modify the AID file with '4F' tag.
|
||||
In that case the data field shall be 4 bytes length and shall contain
|
||||
the new serial number. '4F' is protected by PW3 (admin) PIN.
|
After Width: | Height: | Size: 1007 B |
@ -0,0 +1,516 @@
|
||||
% Options for packages loaded elsewhere
|
||||
\PassOptionsToPackage{unicode$for(hyperrefoptions)$,$hyperrefoptions$$endfor$}{hyperref}
|
||||
\PassOptionsToPackage{hyphens}{url}
|
||||
$if(colorlinks)$
|
||||
\PassOptionsToPackage{dvipsnames,svgnames*,x11names*}{xcolor}
|
||||
$endif$
|
||||
$if(dir)$
|
||||
$if(latex-dir-rtl)$
|
||||
\PassOptionsToPackage{RTLdocument}{bidi}
|
||||
$endif$
|
||||
$endif$
|
||||
$if(CJKmainfont)$
|
||||
\PassOptionsToPackage{space}{xeCJK}
|
||||
$endif$
|
||||
%
|
||||
\documentclass[
|
||||
$if(fontsize)$
|
||||
$fontsize$,
|
||||
$endif$
|
||||
$if(lang)$
|
||||
$babel-lang$,
|
||||
$endif$
|
||||
$if(papersize)$
|
||||
$papersize$paper,
|
||||
$endif$
|
||||
$if(beamer)$
|
||||
ignorenonframetext,
|
||||
$if(handout)$
|
||||
handout,
|
||||
$endif$
|
||||
$if(aspectratio)$
|
||||
aspectratio=$aspectratio$,
|
||||
$endif$
|
||||
$endif$
|
||||
$for(classoption)$
|
||||
$classoption$$sep$,
|
||||
$endfor$
|
||||
]{$documentclass$}
|
||||
$if(beamer)$
|
||||
$if(background-image)$
|
||||
\usebackgroundtemplate{%
|
||||
\includegraphics[width=\paperwidth]{$background-image$}%
|
||||
}
|
||||
$endif$
|
||||
\usepackage{pgfpages}
|
||||
\setbeamertemplate{caption}[numbered]
|
||||
\setbeamertemplate{caption label separator}{: }
|
||||
\setbeamercolor{caption name}{fg=normal text.fg}
|
||||
\beamertemplatenavigationsymbols$if(navigation)$$navigation$$else$empty$endif$
|
||||
$for(beameroption)$
|
||||
\setbeameroption{$beameroption$}
|
||||
$endfor$
|
||||
% Prevent slide breaks in the middle of a paragraph
|
||||
\widowpenalties 1 10000
|
||||
\raggedbottom
|
||||
$if(section-titles)$
|
||||
\setbeamertemplate{part page}{
|
||||
\centering
|
||||
\begin{beamercolorbox}[sep=16pt,center]{part title}
|
||||
\usebeamerfont{part title}\insertpart\par
|
||||
\end{beamercolorbox}
|
||||
}
|
||||
\setbeamertemplate{section page}{
|
||||
\centering
|
||||
\begin{beamercolorbox}[sep=12pt,center]{part title}
|
||||
\usebeamerfont{section title}\insertsection\par
|
||||
\end{beamercolorbox}
|
||||
}
|
||||
\setbeamertemplate{subsection page}{
|
||||
\centering
|
||||
\begin{beamercolorbox}[sep=8pt,center]{part title}
|
||||
\usebeamerfont{subsection title}\insertsubsection\par
|
||||
\end{beamercolorbox}
|
||||
}
|
||||
\AtBeginPart{
|
||||
\frame{\partpage}
|
||||
}
|
||||
\AtBeginSection{
|
||||
\ifbibliography
|
||||
\else
|
||||
\frame{\sectionpage}
|
||||
\fi
|
||||
}
|
||||
\AtBeginSubsection{
|
||||
\frame{\subsectionpage}
|
||||
}
|
||||
$endif$
|
||||
$endif$
|
||||
$if(beamerarticle)$
|
||||
\usepackage{beamerarticle} % needs to be loaded first
|
||||
$endif$
|
||||
$if(fontfamily)$
|
||||
\usepackage[$for(fontfamilyoptions)$$fontfamilyoptions$$sep$,$endfor$]{$fontfamily$}
|
||||
$else$
|
||||
\usepackage{lmodern}
|
||||
$endif$
|
||||
$if(linestretch)$
|
||||
\usepackage{setspace}
|
||||
$endif$
|
||||
\usepackage{amssymb,amsmath}
|
||||
\usepackage{ifxetex,ifluatex}
|
||||
\ifnum 0\ifxetex 1\fi\ifluatex 1\fi=0 % if pdftex
|
||||
\usepackage[$if(fontenc)$$fontenc$$else$T1$endif$]{fontenc}
|
||||
\usepackage[utf8]{inputenc}
|
||||
\usepackage{textcomp} % provide euro and other symbols
|
||||
\else % if luatex or xetex
|
||||
$if(mathspec)$
|
||||
\ifxetex
|
||||
\usepackage{mathspec}
|
||||
\else
|
||||
\usepackage{unicode-math}
|
||||
\fi
|
||||
$else$
|
||||
\usepackage{unicode-math}
|
||||
$endif$
|
||||
\defaultfontfeatures{Scale=MatchLowercase}
|
||||
\defaultfontfeatures[\rmfamily]{Ligatures=TeX,Scale=1}
|
||||
$if(mainfont)$
|
||||
\setmainfont[$for(mainfontoptions)$$mainfontoptions$$sep$,$endfor$]{$mainfont$}
|
||||
$endif$
|
||||
$if(sansfont)$
|
||||
\setsansfont[$for(sansfontoptions)$$sansfontoptions$$sep$,$endfor$]{$sansfont$}
|
||||
$endif$
|
||||
$if(monofont)$
|
||||
\setmonofont[$for(monofontoptions)$$monofontoptions$$sep$,$endfor$]{$monofont$}
|
||||
$endif$
|
||||
$for(fontfamilies)$
|
||||
\newfontfamily{$fontfamilies.name$}[$for(fontfamilies.options)$$fontfamilies.options$$sep$,$endfor$]{$fontfamilies.font$}
|
||||
$endfor$
|
||||
$if(mathfont)$
|
||||
$if(mathspec)$
|
||||
\ifxetex
|
||||
\setmathfont(Digits,Latin,Greek)[$for(mathfontoptions)$$mathfontoptions$$sep$,$endfor$]{$mathfont$}
|
||||
\else
|
||||
\setmathfont[$for(mathfontoptions)$$mathfontoptions$$sep$,$endfor$]{$mathfont$}
|
||||
\fi
|
||||
$else$
|
||||
\setmathfont[$for(mathfontoptions)$$mathfontoptions$$sep$,$endfor$]{$mathfont$}
|
||||
$endif$
|
||||
$endif$
|
||||
$if(CJKmainfont)$
|
||||
\ifxetex
|
||||
\usepackage{xeCJK}
|
||||
\setCJKmainfont[$for(CJKoptions)$$CJKoptions$$sep$,$endfor$]{$CJKmainfont$}
|
||||
\fi
|
||||
$endif$
|
||||
$if(luatexjapresetoptions)$
|
||||
\ifluatex
|
||||
\usepackage[$for(luatexjapresetoptions)$$luatexjapresetoptions$$sep$,$endfor$]{luatexja-preset}
|
||||
\fi
|
||||
$endif$
|
||||
$if(CJKmainfont)$
|
||||
\ifluatex
|
||||
\usepackage[$for(luatexjafontspecoptions)$$luatexjafontspecoptions$$sep$,$endfor$]{luatexja-fontspec}
|
||||
\setmainjfont[$for(CJKoptions)$$CJKoptions$$sep$,$endfor$]{$CJKmainfont$}
|
||||
\fi
|
||||
$endif$
|
||||
\fi
|
||||
$if(beamer)$
|
||||
$if(theme)$
|
||||
\usetheme[$for(themeoptions)$$themeoptions$$sep$,$endfor$]{$theme$}
|
||||
$endif$
|
||||
$if(colortheme)$
|
||||
\usecolortheme{$colortheme$}
|
||||
$endif$
|
||||
$if(fonttheme)$
|
||||
\usefonttheme{$fonttheme$}
|
||||
$endif$
|
||||
$if(mainfont)$
|
||||
\usefonttheme{serif} % use mainfont rather than sansfont for slide text
|
||||
$endif$
|
||||
$if(innertheme)$
|
||||
\useinnertheme{$innertheme$}
|
||||
$endif$
|
||||
$if(outertheme)$
|
||||
\useoutertheme{$outertheme$}
|
||||
$endif$
|
||||
$endif$
|
||||
% Use upquote if available, for straight quotes in verbatim environments
|
||||
\IfFileExists{upquote.sty}{\usepackage{upquote}}{}
|
||||
\IfFileExists{microtype.sty}{% use microtype if available
|
||||
\usepackage[$for(microtypeoptions)$$microtypeoptions$$sep$,$endfor$]{microtype}
|
||||
\UseMicrotypeSet[protrusion]{basicmath} % disable protrusion for tt fonts
|
||||
}{}
|
||||
$if(indent)$
|
||||
$else$
|
||||
\makeatletter
|
||||
\@ifundefined{KOMAClassName}{% if non-KOMA class
|
||||
\IfFileExists{parskip.sty}{%
|
||||
\usepackage{parskip}
|
||||
}{% else
|
||||
\setlength{\parindent}{0pt}
|
||||
\setlength{\parskip}{6pt plus 2pt minus 1pt}}
|
||||
}{% if KOMA class
|
||||
\KOMAoptions{parskip=half}}
|
||||
\makeatother
|
||||
$endif$
|
||||
$if(verbatim-in-note)$
|
||||
\usepackage{fancyvrb}
|
||||
$endif$
|
||||
\usepackage{xcolor}
|
||||
\IfFileExists{xurl.sty}{\usepackage{xurl}}{} % add URL line breaks if available
|
||||
\IfFileExists{bookmark.sty}{\usepackage{bookmark}}{\usepackage{hyperref}}
|
||||
\hypersetup{
|
||||
$if(title-meta)$
|
||||
pdftitle={$title-meta$},
|
||||
$endif$
|
||||
$if(author-meta)$
|
||||
pdfauthor={$author-meta$},
|
||||
$endif$
|
||||
$if(lang)$
|
||||
pdflang={$lang$},
|
||||
$endif$
|
||||
$if(subject)$
|
||||
pdfsubject={$subject$},
|
||||
$endif$
|
||||
$if(keywords)$
|
||||
pdfkeywords={$for(keywords)$$keywords$$sep$, $endfor$},
|
||||
$endif$
|
||||
$if(colorlinks)$
|
||||
colorlinks=true,
|
||||
linkcolor=$if(linkcolor)$$linkcolor$$else$Maroon$endif$,
|
||||
filecolor=$if(filecolor)$$filecolor$$else$Maroon$endif$,
|
||||
citecolor=$if(citecolor)$$citecolor$$else$Blue$endif$,
|
||||
urlcolor=$if(urlcolor)$$urlcolor$$else$Blue$endif$,
|
||||
$else$
|
||||
hidelinks,
|
||||
$endif$
|
||||
pdfcreator={LaTeX via pandoc}}
|
||||
\urlstyle{same} % disable monospaced font for URLs
|
||||
$if(verbatim-in-note)$
|
||||
\VerbatimFootnotes % allow verbatim text in footnotes
|
||||
$endif$
|
||||
$if(geometry)$
|
||||
$if(beamer)$
|
||||
\geometry{$for(geometry)$$geometry$$sep$,$endfor$}
|
||||
$else$
|
||||
\usepackage[$for(geometry)$$geometry$$sep$,$endfor$]{geometry}
|
||||
$endif$
|
||||
$endif$
|
||||
$if(beamer)$
|
||||
\newif\ifbibliography
|
||||
$endif$
|
||||
$if(listings)$
|
||||
\usepackage{listings}
|
||||
\newcommand{\passthrough}[1]{#1}
|
||||
\lstset{defaultdialect=[5.3]Lua}
|
||||
\lstset{defaultdialect=[x86masm]Assembler}
|
||||
$endif$
|
||||
$if(lhs)$
|
||||
\lstnewenvironment{code}{\lstset{language=Haskell,basicstyle=\small\ttfamily}}{}
|
||||
$endif$
|
||||
$if(highlighting-macros)$
|
||||
$highlighting-macros$
|
||||
$endif$
|
||||
$if(tables)$
|
||||
\usepackage{longtable,booktabs}
|
||||
$if(beamer)$
|
||||
\usepackage{caption}
|
||||
% Make caption package work with longtable
|
||||
\makeatletter
|
||||
\def\fnum@table{\tablename~\thetable}
|
||||
\makeatother
|
||||
$else$
|
||||
% Correct order of tables after \paragraph or \subparagraph
|
||||
\usepackage{etoolbox}
|
||||
\makeatletter
|
||||
\patchcmd\longtable{\par}{\if@noskipsec\mbox{}\fi\par}{}{}
|
||||
\makeatother
|
||||
% Allow footnotes in longtable head/foot
|
||||
\IfFileExists{footnotehyper.sty}{\usepackage{footnotehyper}}{\usepackage{footnote}}
|
||||
\makesavenoteenv{longtable}
|
||||
$endif$
|
||||
$endif$
|
||||
$if(graphics)$
|
||||
\usepackage{graphicx}
|
||||
\makeatletter
|
||||
\def\maxwidth{\ifdim\Gin@nat@width>\linewidth\linewidth\else\Gin@nat@width\fi}
|
||||
\def\maxheight{\ifdim\Gin@nat@height>\textheight\textheight\else\Gin@nat@height\fi}
|
||||
\makeatother
|
||||
% Scale images if necessary, so that they will not overflow the page
|
||||
% margins by default, and it is still possible to overwrite the defaults
|
||||
% using explicit options in \includegraphics[width, height, ...]{}
|
||||
\setkeys{Gin}{width=\maxwidth,height=\maxheight,keepaspectratio}
|
||||
% Set default figure placement to htbp
|
||||
\makeatletter
|
||||
\def\fps@figure{htbp}
|
||||
\makeatother
|
||||
$endif$
|
||||
$if(links-as-notes)$
|
||||
% Make links footnotes instead of hotlinks:
|
||||
\DeclareRobustCommand{\href}[2]{#2\footnote{\url{#1}}}
|
||||
$endif$
|
||||
$if(strikeout)$
|
||||
\usepackage[normalem]{ulem}
|
||||
% Avoid problems with \sout in headers with hyperref
|
||||
\pdfstringdefDisableCommands{\renewcommand{\sout}{}}
|
||||
$endif$
|
||||
\setlength{\emergencystretch}{3em} % prevent overfull lines
|
||||
\providecommand{\tightlist}{%
|
||||
\setlength{\itemsep}{0pt}\setlength{\parskip}{0pt}}
|
||||
$if(numbersections)$
|
||||
\setcounter{secnumdepth}{$if(secnumdepth)$$secnumdepth$$else$5$endif$}
|
||||
$else$
|
||||
\setcounter{secnumdepth}{-\maxdimen} % remove section numbering
|
||||
$endif$
|
||||
$if(beamer)$
|
||||
$else$
|
||||
$if(block-headings)$
|
||||
% Make \paragraph and \subparagraph free-standing
|
||||
\ifx\paragraph\undefined\else
|
||||
\let\oldparagraph\paragraph
|
||||
\renewcommand{\paragraph}[1]{\oldparagraph{#1}\mbox{}}
|
||||
\fi
|
||||
\ifx\subparagraph\undefined\else
|
||||
\let\oldsubparagraph\subparagraph
|
||||
\renewcommand{\subparagraph}[1]{\oldsubparagraph{#1}\mbox{}}
|
||||
\fi
|
||||
$endif$
|
||||
$endif$
|
||||
$if(pagestyle)$
|
||||
\pagestyle{$pagestyle$}
|
||||
$endif$
|
||||
$for(header-includes)$
|
||||
$header-includes$
|
||||
$endfor$
|
||||
$if(lang)$
|
||||
\ifxetex
|
||||
% Load polyglossia as late as possible: uses bidi with RTL languages (e.g. Hebrew, Arabic)
|
||||
\usepackage{polyglossia}
|
||||
\setmainlanguage[$polyglossia-lang.options$]{$polyglossia-lang.name$}
|
||||
$for(polyglossia-otherlangs)$
|
||||
\setotherlanguage[$polyglossia-otherlangs.options$]{$polyglossia-otherlangs.name$}
|
||||
$endfor$
|
||||
\else
|
||||
\usepackage[shorthands=off,$for(babel-otherlangs)$$babel-otherlangs$,$endfor$main=$babel-lang$]{babel}
|
||||
$if(babel-newcommands)$
|
||||
$babel-newcommands$
|
||||
$endif$
|
||||
\fi
|
||||
$endif$
|
||||
$if(dir)$
|
||||
\ifxetex
|
||||
% Load bidi as late as possible as it modifies e.g. graphicx
|
||||
\usepackage{bidi}
|
||||
\fi
|
||||
\ifnum 0\ifxetex 1\fi\ifluatex 1\fi=0 % if pdftex
|
||||
\TeXXeTstate=1
|
||||
\newcommand{\RL}[1]{\beginR #1\endR}
|
||||
\newcommand{\LR}[1]{\beginL #1\endL}
|
||||
\newenvironment{RTL}{\beginR}{\endR}
|
||||
\newenvironment{LTR}{\beginL}{\endL}
|
||||
\fi
|
||||
$endif$
|
||||
$if(natbib)$
|
||||
\usepackage[$natbiboptions$]{natbib}
|
||||
\bibliographystyle{$if(biblio-style)$$biblio-style$$else$plainnat$endif$}
|
||||
$endif$
|
||||
$if(biblatex)$
|
||||
\usepackage[$if(biblio-style)$style=$biblio-style$,$endif$$for(biblatexoptions)$$biblatexoptions$$sep$,$endfor$]{biblatex}
|
||||
$for(bibliography)$
|
||||
\addbibresource{$bibliography$}
|
||||
$endfor$
|
||||
$endif$
|
||||
$if(csl-refs)$
|
||||
\newlength{\cslhangindent}
|
||||
\setlength{\cslhangindent}{1.5em}
|
||||
\newenvironment{cslreferences}%
|
||||
{$if(csl-hanging-indent)$\setlength{\parindent}{0pt}%
|
||||
\everypar{\setlength{\hangindent}{\cslhangindent}}\ignorespaces$endif$}%
|
||||
{\par}
|
||||
$endif$
|
||||
|
||||
$if(title)$
|
||||
\title{$title$$if(thanks)$\thanks{$thanks$}$endif$}
|
||||
$endif$
|
||||
$if(subtitle)$
|
||||
$if(beamer)$
|
||||
$else$
|
||||
\usepackage{etoolbox}
|
||||
\makeatletter
|
||||
\providecommand{\subtitle}[1]{% add subtitle to \maketitle
|
||||
\apptocmd{\@title}{\par {\large #1 \par}}{}{}
|
||||
}
|
||||
\makeatother
|
||||
$endif$
|
||||
\subtitle{$subtitle$}
|
||||
$endif$
|
||||
\author{$for(author)$$author$$sep$ \and $endfor$}
|
||||
\date{$date$}
|
||||
$if(beamer)$
|
||||
$if(institute)$
|
||||
\institute{$for(institute)$$institute$$sep$ \and $endfor$}
|
||||
$endif$
|
||||
$if(titlegraphic)$
|
||||
\titlegraphic{\includegraphics{$titlegraphic$}}
|
||||
$endif$
|
||||
$if(logo)$
|
||||
\logo{\includegraphics{$logo$}}
|
||||
$endif$
|
||||
$endif$
|
||||
|
||||
\begin{document}
|
||||
|
||||
\begin{titlepage}
|
||||
\centering
|
||||
{\scshape\Huge OpenPGP Card Application \par}
|
||||
{\scshape \huge Add-on \par}
|
||||
\vspace{1cm}
|
||||
|
||||
{\scshape\LARGE Ledger SAS \par}
|
||||
\vspace{2cm}
|
||||
|
||||
\includegraphics{../LogoLedger.png}
|
||||
|
||||
\vspace{1cm}
|
||||
{\Large\itshape\url {https://github.com/LedgerHQ/app-openpgp}\par}
|
||||
|
||||
\vfill
|
||||
{\large \today\par}
|
||||
\end{titlepage}
|
||||
|
||||
$if(has-frontmatter)$
|
||||
\frontmatter
|
||||
$endif$
|
||||
$if(title)$
|
||||
$if(beamer)$
|
||||
\frame{\titlepage}
|
||||
$else$
|
||||
\maketitle
|
||||
$endif$
|
||||
$if(abstract)$
|
||||
\begin{abstract}
|
||||
$abstract$
|
||||
\end{abstract}
|
||||
$endif$
|
||||
$endif$
|
||||
|
||||
$for(include-before)$
|
||||
$include-before$
|
||||
|
||||
$endfor$
|
||||
$if(toc)$
|
||||
$if(toc-title)$
|
||||
\renewcommand*\contentsname{$toc-title$}
|
||||
$endif$
|
||||
$if(beamer)$
|
||||
\begin{frame}[allowframebreaks]
|
||||
$if(toc-title)$
|
||||
\frametitle{$toc-title$}
|
||||
$endif$
|
||||
\tableofcontents[hideallsubsections]
|
||||
\end{frame}
|
||||
$else$
|
||||
{
|
||||
$if(colorlinks)$
|
||||
\hypersetup{linkcolor=$if(toccolor)$$toccolor$$else$$endif$}
|
||||
$endif$
|
||||
\setcounter{tocdepth}{$toc-depth$}
|
||||
\tableofcontents
|
||||
}
|
||||
$endif$
|
||||
$endif$
|
||||
$if(lot)$
|
||||
\listoftables
|
||||
$endif$
|
||||
$if(lof)$
|
||||
\listoffigures
|
||||
$endif$
|
||||
$if(linestretch)$
|
||||
\setstretch{$linestretch$}
|
||||
$endif$
|
||||
$if(has-frontmatter)$
|
||||
\mainmatter
|
||||
$endif$
|
||||
$body$
|
||||
|
||||
$if(has-frontmatter)$
|
||||
\backmatter
|
||||
$endif$
|
||||
$if(natbib)$
|
||||
$if(bibliography)$
|
||||
$if(biblio-title)$
|
||||
$if(has-chapters)$
|
||||
\renewcommand\bibname{$biblio-title$}
|
||||
$else$
|
||||
\renewcommand\refname{$biblio-title$}
|
||||
$endif$
|
||||
$endif$
|
||||
$if(beamer)$
|
||||
\begin{frame}[allowframebreaks]{$biblio-title$}
|
||||
\bibliographytrue
|
||||
$endif$
|
||||
\bibliography{$for(bibliography)$$bibliography$$sep$,$endfor$}
|
||||
$if(beamer)$
|
||||
\end{frame}
|
||||
$endif$
|
||||
|
||||
$endif$
|
||||
$endif$
|
||||
$if(biblatex)$
|
||||
$if(beamer)$
|
||||
\begin{frame}[allowframebreaks]{$biblio-title$}
|
||||
\bibliographytrue
|
||||
\printbibliography[heading=none]
|
||||
\end{frame}
|
||||
$else$
|
||||
\printbibliography$if(biblio-title)$[title=$biblio-title$]$endif$
|
||||
$endif$
|
||||
|
||||
$endif$
|
||||
$for(include-after)$
|
||||
$include-after$
|
||||
|
||||
$endfor$
|
||||
\end{document}
|
@ -1,326 +0,0 @@
|
||||
License
|
||||
=======
|
||||
|
||||
Author: Cedric Mesnil <cslashm@gmail.com>
|
||||
|
||||
License:
|
||||
|
||||
|
||||
| Copyright 2017 Cedric Mesnil <cslashm@gmail.com>, Ledger SAS
|
||||
|
|
||||
| Licensed under the Apache License, Version 2.0 (the "License");
|
||||
| you may not use this file except in compliance with the License.
|
||||
| You may obtain a copy of the License at
|
||||
|
|
||||
| http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
|
||||
| Unless required by applicable law or agreed to in writing, software
|
||||
| distributed under the License is distributed on an "AS IS" BASIS,
|
||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
| See the License for the specific language governing permissions and
|
||||
| limitations under the License.
|
||||
|
||||
|
||||
|
||||
Introduction
|
||||
============
|
||||
|
||||
OpenPGP Card Application v3.0 add-ons summary
|
||||
---------------------------------------------
|
||||
|
||||
Key management:
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
OpenPGP Application manage four keys for cryptographic operation (PSO) plus two
|
||||
for secure channel.
|
||||
|
||||
The first four keys are defined as follow:
|
||||
- One asymmetric signature private key (RSA or EC), named 'sig';
|
||||
- One asymmetric decryption private key (RSA or EC), named 'dec'
|
||||
- One asymmetric authentication private key (RSA or EC), named 'aut'
|
||||
- One symmetric decryption private key (AES), named 'sym0'
|
||||
|
||||
The 3 first asymmetric keys can be either randomly generated on-card or
|
||||
explicitly put from outside.
|
||||
|
||||
The fourth is put from outside.
|
||||
|
||||
It's never possible to retrieve private key from the card.
|
||||
|
||||
This add-on specification propose a solution to derive those keys from the
|
||||
master seed managed by the Ledger Token.
|
||||
This allow owner to restore a broken token without the needs to keep track of keys
|
||||
outside the card.
|
||||
|
||||
Moreover this add-on specification propose to manage multiple set of the
|
||||
four previously described keys.
|
||||
|
||||
Random number generation
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
OpenPGP Application provides, as optional feature, to generate random bytes.
|
||||
|
||||
This add-on specification propose new type of random generation:
|
||||
- random prime number generation
|
||||
- seeded random number
|
||||
- seeded prime number generation
|
||||
|
||||
|
||||
Key Backup
|
||||
~~~~~~~~~~
|
||||
|
||||
A full keybackup mecanism is provided.
|
||||
|
||||
|
||||
GPG-ledger
|
||||
==========
|
||||
|
||||
Definitions
|
||||
-----------
|
||||
|
||||
- The application is named GPG-ledger
|
||||
- A keys set is named 'keys slot'
|
||||
|
||||
How
|
||||
---
|
||||
|
||||
Deterministic key derivation
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The deterministic key derivation process relies on the BIP32 scheme.
|
||||
The master install path of GPG-ledger is set to /0x80'GPG', aka /80475047
|
||||
Deterministic key derivation maybe activated in:
|
||||
Settings->Seed Mode->Set on
|
||||
|
||||
This activation remains effective until *set off* is selected or the application
|
||||
ends.
|
||||
|
||||
The key management remains the same if seed mode is on or off, i.e. key are stored in memory key containers. So their is no perfomance inpact when using seeded keys.
|
||||
|
||||
Seeded keys are generated as follow:
|
||||
|
||||
**Step1**:
|
||||
|
||||
For a given keys slot n, starting from 1, a seed is first derived with the following path
|
||||
|
||||
Sn = BIP32_derive (/0x80475047/n)
|
||||
|
||||
**Step2**:
|
||||
|
||||
Then specific seeds are derived with the SHA3-XOF function for each of the four key :
|
||||
|
||||
Sk[i] = SHA3-XOF(SHA256(Sn \| <key_name> \| int16(i)), length)
|
||||
|
||||
Sn is the dedicated slot seed from step 1.
|
||||
key_name is one of 'sig ','dec ', 'aut ', 'sym0', each four characters.
|
||||
i is the index, starting from 1, of the desired seed (see below)
|
||||
|
||||
|
||||
**Step 3**:
|
||||
|
||||
*RSA key are generated as follow* :
|
||||
|
||||
Generate two seed Sp, Sq in step2 with :
|
||||
- i € {1,2}
|
||||
- length equals to half key size
|
||||
|
||||
Generate two prime numbers p, q :
|
||||
- p = next_prime(Sp)
|
||||
- q = next_prime(Sq)
|
||||
|
||||
Generate RSA key pair as usual.
|
||||
- choose e
|
||||
- n = p*q
|
||||
- d = inv(e) mod (p-1)(q-1)
|
||||
|
||||
*ECC key genration* :
|
||||
|
||||
Generate one seed Sd in step2 with :
|
||||
- i = 1
|
||||
- length equals to curve size
|
||||
|
||||
Generate ECC key pair :
|
||||
- d = Sd
|
||||
- W = d.G
|
||||
|
||||
|
||||
*AES key generation* :
|
||||
|
||||
Generate one seed Sd in step2 with :
|
||||
- i = 1
|
||||
- length equals to 16
|
||||
|
||||
Generate AES key :
|
||||
- k = Sk
|
||||
|
||||
Deterministic random number
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The deterministic random number generation relies on the BIP32 scheme.
|
||||
The master install path of GPG-ledger is set to /0x80'GPG', aka /80475047
|
||||
|
||||
**Random prime number generation** :
|
||||
|
||||
For a given length *L*:
|
||||
|
||||
- generate random number r of *L* bytes.
|
||||
- generate rp = next_prime(r)
|
||||
- return rp
|
||||
|
||||
**Seeded random number** :
|
||||
|
||||
For a given length *L* and seed *S*:
|
||||
|
||||
- generate Sr = BIP32_derive(/0x80475047/0x0F0F0F0F)
|
||||
- generate r = SHA3-XOF(SHA256(Sr \| 'rnd' \| S), L)
|
||||
- return r
|
||||
|
||||
**Seeded prime number generation** :
|
||||
|
||||
For a given length *L* and seed *S*:
|
||||
|
||||
- generate r as for "Seeded random number"
|
||||
- generate rp = next_prime(r)
|
||||
- return rp
|
||||
|
||||
|
||||
|
||||
Key Backup & Restore
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
In order to backup/restore private key the commands `put_data` and
|
||||
`get_data` accept the tag `B6` (signature key), `B8`(encryption key),
|
||||
`A4` (authentication).
|
||||
|
||||
put_data command accept the exact output of get_data. The get_data command
|
||||
return both the public and private key.
|
||||
|
||||
For security and confidentiality private key is returned encryped in AES.
|
||||
The key used is derived according to previously described AES key derivation
|
||||
with name 'key '.
|
||||
|
||||
|
||||
The data payload is formatted as follow:
|
||||
|
||||
+-------+--------------------------------------------------+
|
||||
| size | Description |
|
||||
+=======+==================================================+
|
||||
| 4 | OS Target ID |
|
||||
+-------+--------------------------------------------------+
|
||||
| 4 | API Level |
|
||||
+-------+--------------------------------------------------+
|
||||
| 4 | compliance Level |
|
||||
+-------+--------------------------------------------------+
|
||||
| 4 | public key size |
|
||||
+-------+--------------------------------------------------+
|
||||
| var | public key |
|
||||
+-------+--------------------------------------------------+
|
||||
| 4 | private key size |
|
||||
+-------+--------------------------------------------------+
|
||||
| var | encrypted private key |
|
||||
+-------+--------------------------------------------------+
|
||||
|
||||
|
||||
APDU Modification
|
||||
-----------------
|
||||
|
||||
Key Slot management
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Key slots are managed by data object 01F1 and 01F2 witch are
|
||||
manageable by PUT/GET DATA command as for others DO and organized as follow.
|
||||
|
||||
On application reset, the *01F2* content is set to *Default Slot* value
|
||||
of *01F1*.
|
||||
|
||||
*01F1:*
|
||||
|
||||
+------+--------------------------------------------------+--------+
|
||||
|bytes | description | R/W |
|
||||
+======+==================================================+========+
|
||||
| 1 | Number of slot | R |
|
||||
+------+--------------------------------------------------+--------+
|
||||
| 2 | Default slot | R/W |
|
||||
+------+--------------------------------------------------+--------+
|
||||
| 3 | Allowed slot selection method | R/W |
|
||||
+------+--------------------------------------------------+--------+
|
||||
|
||||
Byte 3 is endoced as follow:
|
||||
|
||||
+----+----+----+----+----+----+----+----+-------------------------+
|
||||
| b8 | b7 | b6 | b5 | b4 | b3 | b2 | b1 | Meaning |
|
||||
+----+----+----+----+----+----+----+----+-------------------------+
|
||||
| \- | \- | \- | \- | \- | \- | \- | x | selection by APDU |
|
||||
+----+----+----+----+----+----+----+----+-------------------------+
|
||||
| \- | \- | \- | \- | \- | \- | x | \- | selection by screen |
|
||||
+----+----+----+----+----+----+----+----+-------------------------+
|
||||
|
||||
|
||||
|
||||
|
||||
*01F2:*
|
||||
|
||||
+------+--------------------------------------------------+--------+
|
||||
|bytes | Description | R/W |
|
||||
+======+==================================================+========+
|
||||
| 1 | Current slot | R/W |
|
||||
+------+--------------------------------------------------+--------+
|
||||
|
||||
*01F0:*
|
||||
|
||||
+------+--------------------------------------------------+--------+
|
||||
|bytes | Description | R/W |
|
||||
+======+==================================================+========+
|
||||
| 1-3 | 01F1 content | R |
|
||||
+------+--------------------------------------------------+--------+
|
||||
| 4 | 01F2 content | R |
|
||||
+------+--------------------------------------------------+--------+
|
||||
|
||||
|
||||
*Access Conditions:*
|
||||
|
||||
+-------+------------+-------------+
|
||||
| DO | Read | Write |
|
||||
+=======+============+=============+
|
||||
| 01F0 | Always | Never |
|
||||
+-------+------------+-------------+
|
||||
| 01F1 | Always | Verify PW3 |
|
||||
+-------+------------+-------------+
|
||||
| 01F2 | Always | Verify PW2 |
|
||||
+-------+------------+-------------+
|
||||
|
||||
|
||||
|
||||
Deterministic key derivation
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
P2 parameter of GENERATE ASYMMETRIC KEY PAIR is set to (hex value):
|
||||
- 00 for true random key generation
|
||||
- 01 for seeded random key
|
||||
|
||||
|
||||
Deterministic random number
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
P1 parameter of GET CHALLENGE is a bits field encoded as follow:
|
||||
|
||||
+----+-----+----+----+----+----+----+----+-------------------------+
|
||||
| b8 | b7 | b6 | b5 | b4 | b3 | b2 | b1 | Meaning |
|
||||
+----+-----+----+----+----+----+----+----+-------------------------+
|
||||
| \- | \- | \- | \- | \- | \- | \- | x | prime random |
|
||||
+----+-----+----+----+----+----+----+----+-------------------------+
|
||||
| \- | \- | \- | \- | \- | \- | x | \- | seeded random |
|
||||
+----+-----+----+----+----+----+----+----+-------------------------+
|
||||
|
||||
|
||||
When bit b2 is set, data field contains the seed and P2 contains
|
||||
the length of random bytes to generate.
|
||||
|
||||
|
||||
Other minor add-on
|
||||
------------------
|
||||
|
||||
GnuPG use both fingerprints and serial number to identfy key on card.
|
||||
So, the put data command is able to modify the AID file with '4F' tag.
|
||||
In that case the data field shall be four bytes length and shall contain
|
||||
the new serial number. '4F' is protected by PW3 (admin) PIN.
|
@ -1,196 +0,0 @@
|
||||
Step1: ...
|
||||
-----
|
||||
Jump into any temp dir
|
||||
|
||||
|
||||
Step2: install nanos
|
||||
-----
|
||||
Do a fresh install of gpg application 1.1.0 from google app manager
|
||||
|
||||
|
||||
Step3: setup conf
|
||||
-----
|
||||
Create a 'manual-test' directory
|
||||
$ mkdir manual-test
|
||||
|
||||
Create a 'manual-test/gnupg'
|
||||
$ mkdir manual-test/gnupg
|
||||
|
||||
Create a 'manual-test/gnupg/scdaemon.conf' file with content:
|
||||
reader-port "Ledger Token [Nano S] (0001) 01 00"
|
||||
allow-admin
|
||||
card-timeout 1
|
||||
debug-level expert
|
||||
debug 11
|
||||
log-file /tmp/scdaemon.log
|
||||
|
||||
Jump into manual-test dir
|
||||
|
||||
Step4: change to host pin style
|
||||
-----
|
||||
Launch gpg NanoS application and:
|
||||
$ killall scdaemon gpg-agent
|
||||
$ gpg2 --homedir `pwd`/gnupg --card-edit
|
||||
gpg: WARNING: unsafe permissions on homedir '/home/cme/Projects/Git/ledgerblue/blue-app-openpgp-card/manual-test/gnupg'
|
||||
gpg: keybox '/home/cme/Projects/Git/ledgerblue/blue-app-openpgp-card/manual-test/gnupg/pubring.kbx' created
|
||||
|
||||
Reader ...........: Ledger Token [Nano S] (0001) 01 00
|
||||
Application ID ...: D2760001240103002C97DDD38BA90000
|
||||
Version ..........: 3.0
|
||||
Manufacturer .....: unknown
|
||||
Serial number ....: DDD38BA9
|
||||
Name of cardholder: [not set]
|
||||
Language prefs ...: [not set]
|
||||
Sex ..............: unspecified
|
||||
URL of public key : [not set]
|
||||
Login data .......: [not set]
|
||||
Signature PIN ....: not forced
|
||||
Key attributes ...: rsa2048 rsa2048 rsa2048
|
||||
Max. PIN lengths .: 12 12 12
|
||||
PIN retry counter : 3 0 3
|
||||
Signature counter : 0
|
||||
Signature key ....: [none]
|
||||
Encryption key....: [none]
|
||||
Authentication key: [none]
|
||||
General key info..: [none]
|
||||
|
||||
gpg/card> verify
|
||||
|
||||
Reader ...........: Ledger Token [Nano S] (0001) 01 00
|
||||
Application ID ...: D2760001240103002C97DDD38BA90000
|
||||
Version ..........: 3.0
|
||||
Manufacturer .....: unknown
|
||||
Serial number ....: DDD38BA9
|
||||
Name of cardholder: [not set]
|
||||
Language prefs ...: [not set]
|
||||
Sex ..............: unspecified
|
||||
URL of public key : [not set]
|
||||
Login data .......: [not set]
|
||||
Signature PIN ....: not forced
|
||||
Key attributes ...: rsa2048 rsa2048 rsa2048
|
||||
Max. PIN lengths .: 12 12 12
|
||||
PIN retry counter : 3 0 3
|
||||
Signature counter : 0
|
||||
Signature key ....: [none]
|
||||
Encryption key....: [none]
|
||||
Authentication key: [none]
|
||||
General key info..: [none]
|
||||
|
||||
gpg/card>
|
||||
|
||||
Then on nanos, goto settings->PIN mode, and select 'Host'
|
||||
Then on nanos, goto settings->PIN mode, and select 'Set as default'
|
||||
|
||||
unplug and replug the nanos
|
||||
|
||||
relaunch the openpgp application
|
||||
|
||||
Goto settings->PIN mode, and check you have "Host # +" (DASH and PLUS)
|
||||
|
||||
|
||||
Step5: create 2048bits RSA keys
|
||||
-----
|
||||
|
||||
|
||||
In 'manual-test' directory, ask key generation. Nota that during this phase PIN has to be validate on Nanos
|
||||
|
||||
$ killall scdaemon gpg-agent
|
||||
$ gpg2 --homedir `pwd`/gnupg --card-edit
|
||||
gpg: WARNING: unsafe permissions on homedir '/home/cme/Projects/Git/ledgerblue/blue-app-openpgp-card/manual-test/gnupg'
|
||||
|
||||
Reader ...........: Ledger Token [Nano S] (0001) 01 00
|
||||
Application ID ...: D2760001240103002C97DDD38BA90000
|
||||
Version ..........: 3.0
|
||||
Manufacturer .....: unknown
|
||||
Serial number ....: DDD38BA9
|
||||
Name of cardholder: [not set]
|
||||
Language prefs ...: [not set]
|
||||
Sex ..............: unspecified
|
||||
URL of public key : [not set]
|
||||
Login data .......: [not set]
|
||||
Signature PIN ....: not forced
|
||||
Key attributes ...: rsa2048 rsa2048 rsa2048
|
||||
Max. PIN lengths .: 12 12 12
|
||||
PIN retry counter : 3 0 3
|
||||
Signature counter : 0
|
||||
Signature key ....: [none]
|
||||
Encryption key....: [none]
|
||||
Authentication key: [none]
|
||||
General key info..: [none]
|
||||
|
||||
gpg/card> admin
|
||||
Admin commands are allowed
|
||||
|
||||
gpg/card> generate
|
||||
Make off-card backup of encryption key? (Y/n) n
|
||||
|
||||
Please note that the factory settings of the PINs are
|
||||
PIN = '123456' Admin PIN = '12345678'
|
||||
You should change them using the command --change-pin
|
||||
|
||||
What keysize do you want for the Signature key? (2048) 2048
|
||||
What keysize do you want for the Encryption key? (2048) 2048
|
||||
What keysize do you want for the Authentication key? (2048) 2048
|
||||
Please specify how long the key should be valid.
|
||||
0 = key does not expire
|
||||
<n> = key expires in n days
|
||||
<n>w = key expires in n weeks
|
||||
<n>m = key expires in n months
|
||||
<n>y = key expires in n years
|
||||
Key is valid for? (0) 0
|
||||
Key does not expire at all
|
||||
Is this correct? (y/N) y
|
||||
|
||||
GnuPG needs to construct a user ID to identify your key.
|
||||
|
||||
Real name: testkey
|
||||
Email address:
|
||||
Comment:
|
||||
You selected this USER-ID:
|
||||
"testkey"
|
||||
|
||||
Change (N)ame, (C)omment, (E)mail or (O)kay/(Q)uit? O
|
||||
gpg: /home/cme/Projects/Git/ledgerblue/blue-app-openpgp-card/manual-test/gnupg/trustdb.gpg: trustdb created
|
||||
gpg: key 5ED17DF289C757A2 marked as ultimately trusted
|
||||
gpg: directory '/home/cme/Projects/Git/ledgerblue/blue-app-openpgp-card/manual-test/gnupg/openpgp-revocs.d' created
|
||||
gpg: revocation certificate stored as '/home/cme/Projects/Git/ledgerblue/blue-app-openpgp-card/manual-test/gnupg/openpgp-revocs.d/7FDC3D2FCD3558CB06631EAB5ED17DF289C757A2.rev'
|
||||
public and secret key created and signed.
|
||||
|
||||
|
||||
gpg/card> quit
|
||||
pub rsa2048 2017-10-03 [SC]
|
||||
7FDC3D2FCD3558CB06631EAB5ED17DF289C757A2
|
||||
uid testkey
|
||||
sub rsa2048 2017-10-03 [A]
|
||||
sub rsa2047 2017-10-03 [E]
|
||||
|
||||
|
||||
|
||||
Step6: encrypt/decrypt
|
||||
-----
|
||||
encrypt
|
||||
|
||||
$ killall scdaemon gpg-agent
|
||||
$ echo CLEAR > foo.txt
|
||||
$ gpg2 --homedir `pwd`/gnupg -e -r testkey foo.txt
|
||||
gpg: WARNING: unsafe permissions on homedir '/home/cme/Projects/Git/ledgerblue/blue-app-openpgp-card/manual-test/gnupg'
|
||||
gpg: checking the trustdb
|
||||
gpg: marginals needed: 3 completes needed: 1 trust model: pgp
|
||||
gpg: depth: 0 valid: 1 signed: 0 trust: 0-, 0q, 0n, 0m, 0f, 1u
|
||||
|
||||
Force pin to asked
|
||||
|
||||
$ killall gpg-agent scdaemon
|
||||
|
||||
decrypt
|
||||
|
||||
$ gpg2 --homedir `pwd`/gnupg foo.txt.gpg
|
||||
gpg: WARNING: unsafe permissions on homedir '/home/cme/Projects/Git/ledgerblue/blue-app-openpgp-card/manual-test/gnupg'
|
||||
gpg: encrypted with 2047-bit RSA key, ID 602FE5EB7BFA4B00, created 2017-10-03
|
||||
"testkey"
|
||||
File 'foo.txt' exists. Overwrite? (y/N) y
|
||||
|
||||
Step7: pin on screen
|
||||
------
|
||||
|
||||
Restart from Step1, but skip step4
|
@ -0,0 +1,24 @@
|
||||
#!/bin/bash
|
||||
|
||||
NAMES=()
|
||||
NAMES+=(user/app-openpgp)
|
||||
NAMES+=(developer/gpgcard-addon)
|
||||
|
||||
OPTIONS=()
|
||||
OPTIONS+=("--standalone")
|
||||
OPTIONS+=("--from=rst")
|
||||
OPTIONS+=("--to=latex")
|
||||
OPTIONS+=("--variable=papersize:A4")
|
||||
OPTIONS+=("--variable=geometry:margin=1in")
|
||||
OPTIONS+=("--variable=fontsize:10pt")
|
||||
OPTIONS+=("--toc")
|
||||
OPTIONS+=("--number-sections")
|
||||
OPTIONS+=("--template=template.latex")
|
||||
|
||||
for name in "${NAMES[@]}"; do
|
||||
rm -f "${name}.pdf"
|
||||
dir=$(dirname "${name}")
|
||||
file=$(basename "${name}")
|
||||
|
||||
(cd "${dir}"; pandoc ${OPTIONS[@]} --output="${file}.pdf" "${file}.rst")
|
||||
done
|
@ -0,0 +1,33 @@
|
||||
--- libccid_Info.plist_org 2023-10-17 10:50:56.030148081 +0200
|
||||
+++ libccid_Info.plist_test 2024-01-16 15:11:54.798891142 +0100
|
||||
@@ -462,6 +462,8 @@
|
||||
<string>0x2D25</string>
|
||||
<string>0x2C97</string>
|
||||
<string>0x2C97</string>
|
||||
+ <string>0x2C97</string>
|
||||
+ <string>0x2C97</string>
|
||||
<string>0x17EF</string>
|
||||
<string>0x17EF</string>
|
||||
<string>0x17EF</string>
|
||||
@@ -1005,8 +1007,10 @@
|
||||
<string>0x43A9</string>
|
||||
<string>0x0000</string>
|
||||
<string>0x0001</string>
|
||||
- <string>0x0001</string>
|
||||
- <string>0x0004</string>
|
||||
+ <string>0x1009</string>
|
||||
+ <string>0x4009</string>
|
||||
+ <string>0x5009</string>
|
||||
+ <string>0x6009</string>
|
||||
<string>0x6007</string>
|
||||
<string>0x6055</string>
|
||||
<string>0x6111</string>
|
||||
@@ -1552,6 +1556,8 @@
|
||||
<string>KRONEGGER Micro Core Platform</string>
|
||||
<string>Ledger Nano S</string>
|
||||
<string>Ledger Nano X</string>
|
||||
+ <string>Ledger Nano S Plus</string>
|
||||
+ <string>Ledger Stax</string>
|
||||
<string>Lenovo Lenovo USB Smartcard Keyboard</string>
|
||||
<string>Lenovo Lenovo USB Smartcard Keyboard</string>
|
||||
<string>Lenovo Lenovo Smartcard Wired Keyboard II</string>
|
@ -1,275 +0,0 @@
|
||||
\documentclass[$if(fontsize)$$fontsize$,$endif$$if(lang)$$babel-lang$,$endif$$if(papersize)$$papersize$paper,$endif$$for(classoption)$$classoption$$sep$,$endfor$,towside]{report}
|
||||
$if(fontfamily)$
|
||||
\usepackage[$for(fontfamilyoptions)$$fontfamilyoptions$$sep$,$endfor$]{$fontfamily$}
|
||||
$else$
|
||||
\usepackage{lmodern}
|
||||
$endif$
|
||||
$if(linestretch)$
|
||||
\usepackage{setspace}
|
||||
\setstretch{$linestretch$}
|
||||
$endif$
|
||||
|
||||
\usepackage{amssymb,amsmath}
|
||||
\usepackage{ifxetex,ifluatex}
|
||||
\usepackage{fixltx2e} % provides \textsubscript
|
||||
\ifnum 0\ifxetex 1\fi\ifluatex 1\fi=0 % if pdftex
|
||||
\usepackage[$if(fontenc)$$fontenc$$else$T1$endif$]{fontenc}
|
||||
\usepackage[utf8]{inputenc}
|
||||
$if(euro)$
|
||||
\usepackage{eurosym}
|
||||
$endif$
|
||||
\else % if luatex or xelatex
|
||||
\ifxetex
|
||||
\usepackage{mathspec}
|
||||
\else
|
||||
\usepackage{fontspec}
|
||||
\fi
|
||||
\defaultfontfeatures{Ligatures=TeX,Scale=MatchLowercase}
|
||||
$for(fontfamilies)$
|
||||
\newfontfamily{$fontfamilies.name$}[$fontfamilies.options$]{$fontfamilies.font$}
|
||||
$endfor$
|
||||
$if(euro)$
|
||||
\newcommand{\euro}{€}
|
||||
$endif$
|
||||
$if(mainfont)$
|
||||
\setmainfont[$for(mainfontoptions)$$mainfontoptions$$sep$,$endfor$]{$mainfont$}
|
||||
$endif$
|
||||
$if(sansfont)$
|
||||
\setsansfont[$for(sansfontoptions)$$sansfontoptions$$sep$,$endfor$]{$sansfont$}
|
||||
$endif$
|
||||
$if(monofont)$
|
||||
\setmonofont[Mapping=tex-ansi$if(monofontoptions)$,$for(monofontoptions)$$monofontoptions$$sep$,$endfor$$endif$]{$monofont$}
|
||||
$endif$
|
||||
$if(mathfont)$
|
||||
\setmathfont(Digits,Latin,Greek)[$for(mathfontoptions)$$mathfontoptions$$sep$,$endfor$]{$mathfont$}
|
||||
$endif$
|
||||
$if(CJKmainfont)$
|
||||
\usepackage{xeCJK}
|
||||
\setCJKmainfont[$for(CJKoptions)$$CJKoptions$$sep$,$endfor$]{$CJKmainfont$}
|
||||
$endif$
|
||||
\fi
|
||||
% use upquote if available, for straight quotes in verbatim environments
|
||||
\IfFileExists{upquote.sty}{\usepackage{upquote}}{}
|
||||
% use microtype if available
|
||||
\IfFileExists{microtype.sty}{%
|
||||
\usepackage{microtype}
|
||||
\UseMicrotypeSet[protrusion]{basicmath} % disable protrusion for tt fonts
|
||||
}{}
|
||||
|
||||
$if(geometry)$
|
||||
\usepackage[$for(geometry)$$geometry$$sep$,$endfor$]{geometry}
|
||||
$endif$
|
||||
|
||||
\usepackage[unicode=true]{hyperref}
|
||||
$if(colorlinks)$
|
||||
\PassOptionsToPackage{usenames,dvipsnames}{color} % color is loaded by hyperref
|
||||
$endif$
|
||||
\hypersetup{
|
||||
$if(title-meta)$
|
||||
pdftitle={$title-meta$},
|
||||
$endif$
|
||||
$if(author-meta)$
|
||||
pdfauthor={$author-meta$},
|
||||
$endif$
|
||||
$if(keywords)$
|
||||
pdfkeywords={$for(keywords)$$keywords$$sep$; $endfor$},
|
||||
$endif$
|
||||
$if(colorlinks)$
|
||||
colorlinks=true,
|
||||
linkcolor=$if(linkcolor)$$linkcolor$$else$Maroon$endif$,
|
||||
citecolor=$if(citecolor)$$citecolor$$else$Blue$endif$,
|
||||
urlcolor=$if(urlcolor)$$urlcolor$$else$Blue$endif$,
|
||||
$else$
|
||||
pdfborder={0 0 0},
|
||||
$endif$
|
||||
breaklinks=true}
|
||||
\urlstyle{same} % don't use monospace font for urls
|
||||
$if(lang)$
|
||||
\ifnum 0\ifxetex 1\fi\ifluatex 1\fi=0 % if pdftex
|
||||
\usepackage[shorthands=off,$for(babel-otherlangs)$$babel-otherlangs$,$endfor$main=$babel-lang$]{babel}
|
||||
$if(babel-newcommands)$
|
||||
$babel-newcommands$
|
||||
$endif$
|
||||
\else
|
||||
\usepackage{polyglossia}
|
||||
\setmainlanguage[$polyglossia-lang.options$]{$polyglossia-lang.name$}
|
||||
$for(polyglossia-otherlangs)$
|
||||
\setotherlanguage[$polyglossia-otherlangs.options$]{$polyglossia-otherlangs.name$}
|
||||
$endfor$
|
||||
\fi
|
||||
$endif$
|
||||
$if(natbib)$
|
||||
\usepackage{natbib}
|
||||
\bibliographystyle{$if(biblio-style)$$biblio-style$$else$plainnat$endif$}
|
||||
$endif$
|
||||
$if(biblatex)$
|
||||
\usepackage[$if(biblio-style)$style=$biblio-style$,$endif$$for(biblatexoptions)$$biblatexoptions$$sep$,$endfor$]{biblatex}
|
||||
$for(bibliography)$
|
||||
\addbibresource{$bibliography$}
|
||||
$endfor$
|
||||
$endif$
|
||||
$if(listings)$
|
||||
\usepackage{listings}
|
||||
$endif$
|
||||
$if(lhs)$
|
||||
\lstnewenvironment{code}{\lstset{language=Haskell,basicstyle=\small\ttfamily}}{}
|
||||
$endif$
|
||||
$if(highlighting-macros)$
|
||||
$highlighting-macros$
|
||||
$endif$
|
||||
$if(verbatim-in-note)$
|
||||
\usepackage{fancyvrb}
|
||||
\VerbatimFootnotes % allows verbatim text in footnotes
|
||||
$endif$
|
||||
$if(tables)$
|
||||
\usepackage{longtable,booktabs}
|
||||
$endif$
|
||||
$if(graphics)$
|
||||
\usepackage{graphicx,grffile,float}
|
||||
\makeatletter
|
||||
\def\maxwidth{\ifdim\Gin@nat@width>\linewidth\linewidth\else\Gin@nat@width\fi}
|
||||
\def\maxheight{\ifdim\Gin@nat@height>\textheight\textheight\else\Gin@nat@height\fi}
|
||||
\makeatother
|
||||
% Scale images if necessary, so that they will not overflow the page
|
||||
% margins by default, and it is still possible to overwrite the defaults
|
||||
% using explicit options in \includegraphics[width, height, ...]{}
|
||||
\setkeys{Gin}{width=\maxwidth,height=\maxheight,keepaspectratio}
|
||||
$endif$
|
||||
$if(links-as-notes)$
|
||||
% Make links footnotes instead of hotlinks:
|
||||
\renewcommand{\href}[2]{#2\footnote{\url{#1}}}
|
||||
$endif$
|
||||
$if(strikeout)$
|
||||
\usepackage[normalem]{ulem}
|
||||
% avoid problems with \sout in headers with hyperref:
|
||||
\pdfstringdefDisableCommands{\renewcommand{\sout}{}}
|
||||
$endif$
|
||||
$if(indent)$
|
||||
$else$
|
||||
\IfFileExists{parskip.sty}{%
|
||||
\usepackage{parskip}
|
||||
}{% else
|
||||
\setlength{\parindent}{0pt}
|
||||
\setlength{\parskip}{6pt plus 2pt minus 1pt}
|
||||
}
|
||||
$endif$
|
||||
\setlength{\emergencystretch}{3em} % prevent overfull lines
|
||||
\providecommand{\tightlist}{%
|
||||
\setlength{\itemsep}{0pt}\setlength{\parskip}{0pt}}
|
||||
$if(numbersections)$
|
||||
\setcounter{secnumdepth}{$if(secnumdepth)$$secnumdepth$$else$5$endif$}
|
||||
$else$
|
||||
\setcounter{secnumdepth}{0}
|
||||
$endif$
|
||||
$if(subparagraph)$
|
||||
$else$
|
||||
% Redefines (sub)paragraphs to behave more like sections
|
||||
\ifx\paragraph\undefined\else
|
||||
\let\oldparagraph\paragraph
|
||||
\renewcommand{\paragraph}[1]{\oldparagraph{#1}\mbox{}}
|
||||
\fi
|
||||
\ifx\subparagraph\undefined\else
|
||||
\let\oldsubparagraph\subparagraph
|
||||
\renewcommand{\subparagraph}[1]{\oldsubparagraph{#1}\mbox{}}
|
||||
\fi
|
||||
$endif$
|
||||
$if(dir)$
|
||||
\ifxetex
|
||||
% load bidi as late as possible as it modifies e.g. graphicx
|
||||
$if(latex-dir-rtl)$
|
||||
\usepackage[RTLdocument]{bidi}
|
||||
$else$
|
||||
\usepackage{bidi}
|
||||
$endif$
|
||||
\fi
|
||||
\ifnum 0\ifxetex 1\fi\ifluatex 1\fi=0 % if pdftex
|
||||
\TeXXeTstate=1
|
||||
\newcommand{\RL}[1]{\beginR #1\endR}
|
||||
\newcommand{\LR}[1]{\beginL #1\endL}
|
||||
\newenvironment{RTL}{\beginR}{\endR}
|
||||
\newenvironment{LTR}{\beginL}{\endL}
|
||||
\fi
|
||||
$endif$
|
||||
$for(header-includes)$
|
||||
$header-includes$
|
||||
$endfor$
|
||||
|
||||
|
||||
\title{OpenPGP Card Application}
|
||||
\author{Cedric Mesnil cedric@ledger.fr}
|
||||
\date{$date$}
|
||||
|
||||
|
||||
\begin{document}
|
||||
|
||||
\begin{titlepage}
|
||||
\centering
|
||||
% \includegraphics[width=0.15\textwidth]{example-image-1x1}\par\vspace{1cm}
|
||||
{\scshape\LARGE OpenPGP Card Application \par}
|
||||
{\scshape \LARGE User Guide \par}
|
||||
\vspace{1cm}
|
||||
|
||||
% {\scshape\Large Ledger SAS \par}
|
||||
\vspace{1cm}
|
||||
\begin{figure}[h]
|
||||
\includegraphics{../common/LogoLedgerV.png}
|
||||
\centering
|
||||
\end{figure}
|
||||
{\Large\itshape Cédric Mesnil (cedric@ledger.fr)\par}
|
||||
\vfill
|
||||
|
||||
% Bottom of the page
|
||||
{\large \today\par}
|
||||
\end{titlepage}
|
||||
|
||||
|
||||
|
||||
$if(abstract)$
|
||||
\begin{abstract}
|
||||
$abstract$
|
||||
\end{abstract}
|
||||
$endif$
|
||||
|
||||
$for(include-before)$
|
||||
$include-before$
|
||||
|
||||
$endfor$
|
||||
$if(toc)$
|
||||
{
|
||||
$if(colorlinks)$
|
||||
\hypersetup{linkcolor=$if(toccolor)$$toccolor$$else$black$endif$}
|
||||
$endif$
|
||||
\setcounter{tocdepth}{$toc-depth$}
|
||||
\tableofcontents
|
||||
}
|
||||
$endif$
|
||||
$if(lot)$
|
||||
\listoftables
|
||||
$endif$
|
||||
$if(lof)$
|
||||
\listoffigures
|
||||
$endif$
|
||||
$body$
|
||||
|
||||
$if(natbib)$
|
||||
$if(bibliography)$
|
||||
$if(biblio-title)$
|
||||
$if(book-class)$
|
||||
\renewcommand\bibname{$biblio-title$}
|
||||
$else$
|
||||
\renewcommand\refname{$biblio-title$}
|
||||
$endif$
|
||||
$endif$
|
||||
\bibliography{$for(bibliography)$$bibliography$$sep$,$endfor$}
|
||||
|
||||
$endif$
|
||||
$endif$
|
||||
$if(biblatex)$
|
||||
\printbibliography$if(biblio-title)$[title=$biblio-title$]$endif$
|
||||
|
||||
$endif$
|
||||
$for(include-after)$
|
||||
$include-after$
|
||||
|
||||
$endfor$
|
||||
\end{document}
|
@ -1,9 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
NAME=blue-app-openpgp-card
|
||||
|
||||
rm -f ${NAME}.pdf
|
||||
|
||||
OUTPUT_FORMAT=rst+smart
|
||||
|
||||
pandoc -s --template=${NAME}.template -f ${OUTPUT_FORMAT} -V geometry:a4paper -V geometry:margin=1in -V fontsize=10pt -t latex --toc -N -o ${NAME}.pdf ${NAME}.rst
|
@ -0,0 +1,520 @@
|
||||
% Options for packages loaded elsewhere
|
||||
\PassOptionsToPackage{unicode$for(hyperrefoptions)$,$hyperrefoptions$$endfor$}{hyperref}
|
||||
\PassOptionsToPackage{hyphens}{url}
|
||||
$if(colorlinks)$
|
||||
\PassOptionsToPackage{dvipsnames,svgnames*,x11names*}{xcolor}
|
||||
$endif$
|
||||
$if(dir)$
|
||||
$if(latex-dir-rtl)$
|
||||
\PassOptionsToPackage{RTLdocument}{bidi}
|
||||
$endif$
|
||||
$endif$
|
||||
$if(CJKmainfont)$
|
||||
\PassOptionsToPackage{space}{xeCJK}
|
||||
$endif$
|
||||
%
|
||||
\documentclass[
|
||||
$if(fontsize)$
|
||||
$fontsize$,
|
||||
$endif$
|
||||
$if(lang)$
|
||||
$babel-lang$,
|
||||
$endif$
|
||||
$if(papersize)$
|
||||
$papersize$paper,
|
||||
$endif$
|
||||
$if(beamer)$
|
||||
ignorenonframetext,
|
||||
$if(handout)$
|
||||
handout,
|
||||
$endif$
|
||||
$if(aspectratio)$
|
||||
aspectratio=$aspectratio$,
|
||||
$endif$
|
||||
$endif$
|
||||
$for(classoption)$
|
||||
$classoption$$sep$,
|
||||
$endfor$
|
||||
]{$documentclass$}
|
||||
$if(beamer)$
|
||||
$if(background-image)$
|
||||
\usebackgroundtemplate{%
|
||||
\includegraphics[width=\paperwidth]{$background-image$}%
|
||||
}
|
||||
$endif$
|
||||
\usepackage{pgfpages}
|
||||
\setbeamertemplate{caption}[numbered]
|
||||
\setbeamertemplate{caption label separator}{: }
|
||||
\setbeamercolor{caption name}{fg=normal text.fg}
|
||||
\beamertemplatenavigationsymbols$if(navigation)$$navigation$$else$empty$endif$
|
||||
$for(beameroption)$
|
||||
\setbeameroption{$beameroption$}
|
||||
$endfor$
|
||||
% Prevent slide breaks in the middle of a paragraph
|
||||
\widowpenalties 1 10000
|
||||
\raggedbottom
|
||||
$if(section-titles)$
|
||||
\setbeamertemplate{part page}{
|
||||
\centering
|
||||
\begin{beamercolorbox}[sep=16pt,center]{part title}
|
||||
\usebeamerfont{part title}\insertpart\par
|
||||
\end{beamercolorbox}
|
||||
}
|
||||
\setbeamertemplate{section page}{
|
||||
\centering
|
||||
\begin{beamercolorbox}[sep=12pt,center]{part title}
|
||||
\usebeamerfont{section title}\insertsection\par
|
||||
\end{beamercolorbox}
|
||||
}
|
||||
\setbeamertemplate{subsection page}{
|
||||
\centering
|
||||
\begin{beamercolorbox}[sep=8pt,center]{part title}
|
||||
\usebeamerfont{subsection title}\insertsubsection\par
|
||||
\end{beamercolorbox}
|
||||
}
|
||||
\AtBeginPart{
|
||||
\frame{\partpage}
|
||||
}
|
||||
\AtBeginSection{
|
||||
\ifbibliography
|
||||
\else
|
||||
\frame{\sectionpage}
|
||||
\fi
|
||||
}
|
||||
\AtBeginSubsection{
|
||||
\frame{\subsectionpage}
|
||||
}
|
||||
$endif$
|
||||
$endif$
|
||||
$if(beamerarticle)$
|
||||
\usepackage{beamerarticle} % needs to be loaded first
|
||||
$endif$
|
||||
$if(fontfamily)$
|
||||
\usepackage[$for(fontfamilyoptions)$$fontfamilyoptions$$sep$,$endfor$]{$fontfamily$}
|
||||
$else$
|
||||
\usepackage{lmodern}
|
||||
$endif$
|
||||
$if(linestretch)$
|
||||
\usepackage{setspace}
|
||||
$endif$
|
||||
\usepackage{amssymb,amsmath}
|
||||
\usepackage{ifxetex,ifluatex}
|
||||
\usepackage{pifont}
|
||||
\usepackage{newunicodechar}
|
||||
\newunicodechar{✓}{\ding{51}}
|
||||
\newunicodechar{✗}{\ding{55}}
|
||||
\ifnum 0\ifxetex 1\fi\ifluatex 1\fi=0 % if pdftex
|
||||
\usepackage[$if(fontenc)$$fontenc$$else$T1$endif$]{fontenc}
|
||||
\usepackage[utf8]{inputenc}
|
||||
\usepackage{textcomp} % provide euro and other symbols
|
||||
\else % if luatex or xetex
|
||||
$if(mathspec)$
|
||||
\ifxetex
|
||||
\usepackage{mathspec}
|
||||
\else
|
||||
\usepackage{unicode-math}
|
||||
\fi
|
||||
$else$
|
||||
\usepackage{unicode-math}
|
||||
$endif$
|
||||
\defaultfontfeatures{Scale=MatchLowercase}
|
||||
\defaultfontfeatures[\rmfamily]{Ligatures=TeX,Scale=1}
|
||||
$if(mainfont)$
|
||||
\setmainfont[$for(mainfontoptions)$$mainfontoptions$$sep$,$endfor$]{$mainfont$}
|
||||
$endif$
|
||||
$if(sansfont)$
|
||||
\setsansfont[$for(sansfontoptions)$$sansfontoptions$$sep$,$endfor$]{$sansfont$}
|
||||
$endif$
|
||||
$if(monofont)$
|
||||
\setmonofont[$for(monofontoptions)$$monofontoptions$$sep$,$endfor$]{$monofont$}
|
||||
$endif$
|
||||
$for(fontfamilies)$
|
||||
\newfontfamily{$fontfamilies.name$}[$for(fontfamilies.options)$$fontfamilies.options$$sep$,$endfor$]{$fontfamilies.font$}
|
||||
$endfor$
|
||||
$if(mathfont)$
|
||||
$if(mathspec)$
|
||||
\ifxetex
|
||||
\setmathfont(Digits,Latin,Greek)[$for(mathfontoptions)$$mathfontoptions$$sep$,$endfor$]{$mathfont$}
|
||||
\else
|
||||
\setmathfont[$for(mathfontoptions)$$mathfontoptions$$sep$,$endfor$]{$mathfont$}
|
||||
\fi
|
||||
$else$
|
||||
\setmathfont[$for(mathfontoptions)$$mathfontoptions$$sep$,$endfor$]{$mathfont$}
|
||||
$endif$
|
||||
$endif$
|
||||
$if(CJKmainfont)$
|
||||
\ifxetex
|
||||
\usepackage{xeCJK}
|
||||
\setCJKmainfont[$for(CJKoptions)$$CJKoptions$$sep$,$endfor$]{$CJKmainfont$}
|
||||
\fi
|
||||
$endif$
|
||||
$if(luatexjapresetoptions)$
|
||||
\ifluatex
|
||||
\usepackage[$for(luatexjapresetoptions)$$luatexjapresetoptions$$sep$,$endfor$]{luatexja-preset}
|
||||
\fi
|
||||
$endif$
|
||||
$if(CJKmainfont)$
|
||||
\ifluatex
|
||||
\usepackage[$for(luatexjafontspecoptions)$$luatexjafontspecoptions$$sep$,$endfor$]{luatexja-fontspec}
|
||||
\setmainjfont[$for(CJKoptions)$$CJKoptions$$sep$,$endfor$]{$CJKmainfont$}
|
||||
\fi
|
||||
$endif$
|
||||
\fi
|
||||
$if(beamer)$
|
||||
$if(theme)$
|
||||
\usetheme[$for(themeoptions)$$themeoptions$$sep$,$endfor$]{$theme$}
|
||||
$endif$
|
||||
$if(colortheme)$
|
||||
\usecolortheme{$colortheme$}
|
||||
$endif$
|
||||
$if(fonttheme)$
|
||||
\usefonttheme{$fonttheme$}
|
||||
$endif$
|
||||
$if(mainfont)$
|
||||
\usefonttheme{serif} % use mainfont rather than sansfont for slide text
|
||||
$endif$
|
||||
$if(innertheme)$
|
||||
\useinnertheme{$innertheme$}
|
||||
$endif$
|
||||
$if(outertheme)$
|
||||
\useoutertheme{$outertheme$}
|
||||
$endif$
|
||||
$endif$
|
||||
% Use upquote if available, for straight quotes in verbatim environments
|
||||
\IfFileExists{upquote.sty}{\usepackage{upquote}}{}
|
||||
\IfFileExists{microtype.sty}{% use microtype if available
|
||||
\usepackage[$for(microtypeoptions)$$microtypeoptions$$sep$,$endfor$]{microtype}
|
||||
\UseMicrotypeSet[protrusion]{basicmath} % disable protrusion for tt fonts
|
||||
}{}
|
||||
$if(indent)$
|
||||
$else$
|
||||
\makeatletter
|
||||
\@ifundefined{KOMAClassName}{% if non-KOMA class
|
||||
\IfFileExists{parskip.sty}{%
|
||||
\usepackage{parskip}
|
||||
}{% else
|
||||
\setlength{\parindent}{0pt}
|
||||
\setlength{\parskip}{6pt plus 2pt minus 1pt}}
|
||||
}{% if KOMA class
|
||||
\KOMAoptions{parskip=half}}
|
||||
\makeatother
|
||||
$endif$
|
||||
$if(verbatim-in-note)$
|
||||
\usepackage{fancyvrb}
|
||||
$endif$
|
||||
\usepackage{xcolor}
|
||||
\IfFileExists{xurl.sty}{\usepackage{xurl}}{} % add URL line breaks if available
|
||||
\IfFileExists{bookmark.sty}{\usepackage{bookmark}}{\usepackage{hyperref}}
|
||||
\hypersetup{
|
||||
$if(title-meta)$
|
||||
pdftitle={$title-meta$},
|
||||
$endif$
|
||||
$if(author-meta)$
|
||||
pdfauthor={$author-meta$},
|
||||
$endif$
|
||||
$if(lang)$
|
||||
pdflang={$lang$},
|
||||
$endif$
|
||||
$if(subject)$
|
||||
pdfsubject={$subject$},
|
||||
$endif$
|
||||
$if(keywords)$
|
||||
pdfkeywords={$for(keywords)$$keywords$$sep$, $endfor$},
|
||||
$endif$
|
||||
$if(colorlinks)$
|
||||
colorlinks=true,
|
||||
linkcolor=$if(linkcolor)$$linkcolor$$else$Maroon$endif$,
|
||||
filecolor=$if(filecolor)$$filecolor$$else$Maroon$endif$,
|
||||
citecolor=$if(citecolor)$$citecolor$$else$Blue$endif$,
|
||||
urlcolor=$if(urlcolor)$$urlcolor$$else$Blue$endif$,
|
||||
$else$
|
||||
hidelinks,
|
||||
$endif$
|
||||
pdfcreator={LaTeX via pandoc}}
|
||||
\urlstyle{same} % disable monospaced font for URLs
|
||||
$if(verbatim-in-note)$
|
||||
\VerbatimFootnotes % allow verbatim text in footnotes
|
||||
$endif$
|
||||
$if(geometry)$
|
||||
$if(beamer)$
|
||||
\geometry{$for(geometry)$$geometry$$sep$,$endfor$}
|
||||
$else$
|
||||
\usepackage[$for(geometry)$$geometry$$sep$,$endfor$]{geometry}
|
||||
$endif$
|
||||
$endif$
|
||||
$if(beamer)$
|
||||
\newif\ifbibliography
|
||||
$endif$
|
||||
$if(listings)$
|
||||
\usepackage{listings}
|
||||
\newcommand{\passthrough}[1]{#1}
|
||||
\lstset{defaultdialect=[5.3]Lua}
|
||||
\lstset{defaultdialect=[x86masm]Assembler}
|
||||
$endif$
|
||||
$if(lhs)$
|
||||
\lstnewenvironment{code}{\lstset{language=Haskell,basicstyle=\small\ttfamily}}{}
|
||||
$endif$
|
||||
$if(highlighting-macros)$
|
||||
$highlighting-macros$
|
||||
$endif$
|
||||
$if(tables)$
|
||||
\usepackage{longtable,booktabs}
|
||||
$if(beamer)$
|
||||
\usepackage{caption}
|
||||
% Make caption package work with longtable
|
||||
\makeatletter
|
||||
\def\fnum@table{\tablename~\thetable}
|
||||
\makeatother
|
||||
$else$
|
||||
% Correct order of tables after \paragraph or \subparagraph
|
||||
\usepackage{etoolbox}
|
||||
\makeatletter
|
||||
\patchcmd\longtable{\par}{\if@noskipsec\mbox{}\fi\par}{}{}
|
||||
\makeatother
|
||||
% Allow footnotes in longtable head/foot
|
||||
\IfFileExists{footnotehyper.sty}{\usepackage{footnotehyper}}{\usepackage{footnote}}
|
||||
\makesavenoteenv{longtable}
|
||||
$endif$
|
||||
$endif$
|
||||
$if(graphics)$
|
||||
\usepackage{graphicx}
|
||||
\makeatletter
|
||||
\def\maxwidth{\ifdim\Gin@nat@width>\linewidth\linewidth\else\Gin@nat@width\fi}
|
||||
\def\maxheight{\ifdim\Gin@nat@height>\textheight\textheight\else\Gin@nat@height\fi}
|
||||
\makeatother
|
||||
% Scale images if necessary, so that they will not overflow the page
|
||||
% margins by default, and it is still possible to overwrite the defaults
|
||||
% using explicit options in \includegraphics[width, height, ...]{}
|
||||
\setkeys{Gin}{width=\maxwidth,height=\maxheight,keepaspectratio}
|
||||
% Set default figure placement to htbp
|
||||
\makeatletter
|
||||
\def\fps@figure{htbp}
|
||||
\makeatother
|
||||
$endif$
|
||||
$if(links-as-notes)$
|
||||
% Make links footnotes instead of hotlinks:
|
||||
\DeclareRobustCommand{\href}[2]{#2\footnote{\url{#1}}}
|
||||
$endif$
|
||||
$if(strikeout)$
|
||||
\usepackage[normalem]{ulem}
|
||||
% Avoid problems with \sout in headers with hyperref
|
||||
\pdfstringdefDisableCommands{\renewcommand{\sout}{}}
|
||||
$endif$
|
||||
\setlength{\emergencystretch}{3em} % prevent overfull lines
|
||||
\providecommand{\tightlist}{%
|
||||
\setlength{\itemsep}{0pt}\setlength{\parskip}{0pt}}
|
||||
$if(numbersections)$
|
||||
\setcounter{secnumdepth}{$if(secnumdepth)$$secnumdepth$$else$5$endif$}
|
||||
$else$
|
||||
\setcounter{secnumdepth}{-\maxdimen} % remove section numbering
|
||||
$endif$
|
||||
$if(beamer)$
|
||||
$else$
|
||||
$if(block-headings)$
|
||||
% Make \paragraph and \subparagraph free-standing
|
||||
\ifx\paragraph\undefined\else
|
||||
\let\oldparagraph\paragraph
|
||||
\renewcommand{\paragraph}[1]{\oldparagraph{#1}\mbox{}}
|
||||
\fi
|
||||
\ifx\subparagraph\undefined\else
|
||||
\let\oldsubparagraph\subparagraph
|
||||
\renewcommand{\subparagraph}[1]{\oldsubparagraph{#1}\mbox{}}
|
||||
\fi
|
||||
$endif$
|
||||
$endif$
|
||||
$if(pagestyle)$
|
||||
\pagestyle{$pagestyle$}
|
||||
$endif$
|
||||
$for(header-includes)$
|
||||
$header-includes$
|
||||
$endfor$
|
||||
$if(lang)$
|
||||
\ifxetex
|
||||
% Load polyglossia as late as possible: uses bidi with RTL languages (e.g. Hebrew, Arabic)
|
||||
\usepackage{polyglossia}
|
||||
\setmainlanguage[$polyglossia-lang.options$]{$polyglossia-lang.name$}
|
||||
$for(polyglossia-otherlangs)$
|
||||
\setotherlanguage[$polyglossia-otherlangs.options$]{$polyglossia-otherlangs.name$}
|
||||
$endfor$
|
||||
\else
|
||||
\usepackage[shorthands=off,$for(babel-otherlangs)$$babel-otherlangs$,$endfor$main=$babel-lang$]{babel}
|
||||
$if(babel-newcommands)$
|
||||
$babel-newcommands$
|
||||
$endif$
|
||||
\fi
|
||||
$endif$
|
||||
$if(dir)$
|
||||
\ifxetex
|
||||
% Load bidi as late as possible as it modifies e.g. graphicx
|
||||
\usepackage{bidi}
|
||||
\fi
|
||||
\ifnum 0\ifxetex 1\fi\ifluatex 1\fi=0 % if pdftex
|
||||
\TeXXeTstate=1
|
||||
\newcommand{\RL}[1]{\beginR #1\endR}
|
||||
\newcommand{\LR}[1]{\beginL #1\endL}
|
||||
\newenvironment{RTL}{\beginR}{\endR}
|
||||
\newenvironment{LTR}{\beginL}{\endL}
|
||||
\fi
|
||||
$endif$
|
||||
$if(natbib)$
|
||||
\usepackage[$natbiboptions$]{natbib}
|
||||
\bibliographystyle{$if(biblio-style)$$biblio-style$$else$plainnat$endif$}
|
||||
$endif$
|
||||
$if(biblatex)$
|
||||
\usepackage[$if(biblio-style)$style=$biblio-style$,$endif$$for(biblatexoptions)$$biblatexoptions$$sep$,$endfor$]{biblatex}
|
||||
$for(bibliography)$
|
||||
\addbibresource{$bibliography$}
|
||||
$endfor$
|
||||
$endif$
|
||||
$if(csl-refs)$
|
||||
\newlength{\cslhangindent}
|
||||
\setlength{\cslhangindent}{1.5em}
|
||||
\newenvironment{cslreferences}%
|
||||
{$if(csl-hanging-indent)$\setlength{\parindent}{0pt}%
|
||||
\everypar{\setlength{\hangindent}{\cslhangindent}}\ignorespaces$endif$}%
|
||||
{\par}
|
||||
$endif$
|
||||
|
||||
$if(title)$
|
||||
\title{$title$$if(thanks)$\thanks{$thanks$}$endif$}
|
||||
$endif$
|
||||
$if(subtitle)$
|
||||
$if(beamer)$
|
||||
$else$
|
||||
\usepackage{etoolbox}
|
||||
\makeatletter
|
||||
\providecommand{\subtitle}[1]{% add subtitle to \maketitle
|
||||
\apptocmd{\@title}{\par {\large #1 \par}}{}{}
|
||||
}
|
||||
\makeatother
|
||||
$endif$
|
||||
\subtitle{$subtitle$}
|
||||
$endif$
|
||||
\author{$for(author)$$author$$sep$ \and $endfor$}
|
||||
\date{$date$}
|
||||
$if(beamer)$
|
||||
$if(institute)$
|
||||
\institute{$for(institute)$$institute$$sep$ \and $endfor$}
|
||||
$endif$
|
||||
$if(titlegraphic)$
|
||||
\titlegraphic{\includegraphics{$titlegraphic$}}
|
||||
$endif$
|
||||
$if(logo)$
|
||||
\logo{\includegraphics{$logo$}}
|
||||
$endif$
|
||||
$endif$
|
||||
|
||||
\begin{document}
|
||||
|
||||
\begin{titlepage}
|
||||
\centering
|
||||
{\scshape\Huge OpenPGP Card Application \par}
|
||||
{\scshape \huge User Guide \par}
|
||||
\vspace{1cm}
|
||||
|
||||
{\scshape\LARGE Ledger SAS \par}
|
||||
\vspace{2cm}
|
||||
|
||||
\includegraphics{../LogoLedger.png}
|
||||
|
||||
\vspace{1cm}
|
||||
{\Large\itshape\url {https://github.com/LedgerHQ/app-openpgp}\par}
|
||||
|
||||
\vfill
|
||||
{\large \today\par}
|
||||
\end{titlepage}
|
||||
|
||||
$if(has-frontmatter)$
|
||||
\frontmatter
|
||||
$endif$
|
||||
$if(title)$
|
||||
$if(beamer)$
|
||||
\frame{\titlepage}
|
||||
$else$
|
||||
\maketitle
|
||||
$endif$
|
||||
$if(abstract)$
|
||||
\begin{abstract}
|
||||
$abstract$
|
||||
\end{abstract}
|
||||
$endif$
|
||||
$endif$
|
||||
|
||||
$for(include-before)$
|
||||
$include-before$
|
||||
|
||||
$endfor$
|
||||
$if(toc)$
|
||||
$if(toc-title)$
|
||||
\renewcommand*\contentsname{$toc-title$}
|
||||
$endif$
|
||||
$if(beamer)$
|
||||
\begin{frame}[allowframebreaks]
|
||||
$if(toc-title)$
|
||||
\frametitle{$toc-title$}
|
||||
$endif$
|
||||
\tableofcontents[hideallsubsections]
|
||||
\end{frame}
|
||||
$else$
|
||||
{
|
||||
$if(colorlinks)$
|
||||
\hypersetup{linkcolor=$if(toccolor)$$toccolor$$else$$endif$}
|
||||
$endif$
|
||||
\setcounter{tocdepth}{$toc-depth$}
|
||||
\tableofcontents
|
||||
}
|
||||
$endif$
|
||||
$endif$
|
||||
$if(lot)$
|
||||
\listoftables
|
||||
$endif$
|
||||
$if(lof)$
|
||||
\listoffigures
|
||||
$endif$
|
||||
$if(linestretch)$
|
||||
\setstretch{$linestretch$}
|
||||
$endif$
|
||||
$if(has-frontmatter)$
|
||||
\mainmatter
|
||||
$endif$
|
||||
$body$
|
||||
|
||||
$if(has-frontmatter)$
|
||||
\backmatter
|
||||
$endif$
|
||||
$if(natbib)$
|
||||
$if(bibliography)$
|
||||
$if(biblio-title)$
|
||||
$if(has-chapters)$
|
||||
\renewcommand\bibname{$biblio-title$}
|
||||
$else$
|
||||
\renewcommand\refname{$biblio-title$}
|
||||
$endif$
|
||||
$endif$
|
||||
$if(beamer)$
|
||||
\begin{frame}[allowframebreaks]{$biblio-title$}
|
||||
\bibliographytrue
|
||||
$endif$
|
||||
\bibliography{$for(bibliography)$$bibliography$$sep$,$endfor$}
|
||||
$if(beamer)$
|
||||
\end{frame}
|
||||
$endif$
|
||||
|
||||
$endif$
|
||||
$endif$
|
||||
$if(biblatex)$
|
||||
$if(beamer)$
|
||||
\begin{frame}[allowframebreaks]{$biblio-title$}
|
||||
\bibliographytrue
|
||||
\printbibliography[heading=none]
|
||||
\end{frame}
|
||||
$else$
|
||||
\printbibliography$if(biblio-title)$[title=$biblio-title$]$endif$
|
||||
$endif$
|
||||
|
||||
$endif$
|
||||
$for(include-after)$
|
||||
$include-after$
|
||||
|
||||
$endfor$
|
||||
\end{document}
|
Before Width: | Height: | Size: 74 B |
@ -0,0 +1 @@
|
||||
../icons/gpg_16px.gif
|
After Width: | Height: | Size: 348 B |
Before Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 83 B |
Before Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 105 B |
After Width: | Height: | Size: 112 B |
After Width: | Height: | Size: 283 B |
@ -1,11 +0,0 @@
|
||||
LICENSE
|
||||
|
||||
|
||||
|
||||
Files "manager_gnupg.xcf" and "manager_gnupg.png" are covered by Creative Commons Attribution-ShareAlike 3.0 Unported License.
|
||||
See https://creativecommons.org/licenses/by-sa/3.0/legalcode .
|
||||
Thanks to gnupg.org for the original images.
|
||||
|
||||
|
||||
Others under this directory are covered by Apache License Version 2.0,
|
||||
|
Before Width: | Height: | Size: 88 B |
Before Width: | Height: | Size: 83 B |
Before Width: | Height: | Size: 4.0 KiB |
@ -0,0 +1,8 @@
|
||||
[app]
|
||||
build_directory = "./"
|
||||
sdk = "C"
|
||||
devices = ["nanos", "nanox", "nanos+", "stax"]
|
||||
|
||||
[tests]
|
||||
unit_directory = "./unit-tests/"
|
||||
pytest_directory = "./tests/"
|
@ -0,0 +1,210 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# script to check OpenPGP Application features
|
||||
#
|
||||
|
||||
exeName=$(readlink "$0")
|
||||
[[ -z ${exeName} ]] && exeName=$0
|
||||
dirName=$(dirname "${exeName}")
|
||||
|
||||
gnupg_home_dir="$(realpath "${dirName}/gnupg")"
|
||||
|
||||
VERBOSE=false
|
||||
EXPERT=false
|
||||
|
||||
#===============================================================================
|
||||
#
|
||||
# help - Prints script help and usage
|
||||
#
|
||||
#===============================================================================
|
||||
# shellcheck disable=SC2154 # var is referenced but not assigned
|
||||
help() {
|
||||
echo
|
||||
echo "Usage: ${exeName} <options>"
|
||||
echo
|
||||
echo "Options:"
|
||||
echo
|
||||
echo " -c <init|reset|card|encrypt|decryptsign|verify> : Requested command"
|
||||
echo " -e : Expert mode mode"
|
||||
echo " -v : Verbose mode"
|
||||
echo " -h : Displays this help"
|
||||
echo
|
||||
exit 1
|
||||
}
|
||||
|
||||
#===============================================================================
|
||||
#
|
||||
# reset - Kill running process, ensure clear next operation
|
||||
#
|
||||
#===============================================================================
|
||||
reset() {
|
||||
# Kill running process
|
||||
killall scdaemon gpg-agent 2>/dev/null
|
||||
}
|
||||
|
||||
#===============================================================================
|
||||
#
|
||||
# init - Init the gnupg config, start from an empty keyring
|
||||
#
|
||||
#===============================================================================
|
||||
init() {
|
||||
reset
|
||||
|
||||
# Cleanup old gnupg home directory
|
||||
dir=$(basename "${gnupg_home_dir}")
|
||||
rm -fr "${dir}" foo.txt*
|
||||
mkdir "${dir}"
|
||||
chmod 700 "${dir}"
|
||||
|
||||
{
|
||||
echo reader-port \"Ledger token\"
|
||||
echo allow-admin
|
||||
echo enable-pinpad-varlen
|
||||
echo card-timeout 1
|
||||
} > "${dir}/scdaemon.conf"
|
||||
|
||||
if [[ ${EXPERT} == true ]]; then
|
||||
{
|
||||
echo log-file /tmp/scd.log
|
||||
echo debug-level guru
|
||||
echo debug-all
|
||||
} >> "${dir}/scdaemon.conf"
|
||||
fi
|
||||
|
||||
gpgconf --reload scdaemon
|
||||
}
|
||||
|
||||
#===============================================================================
|
||||
#
|
||||
# card - Show/edit the card status and configuration
|
||||
#
|
||||
#===============================================================================
|
||||
card() {
|
||||
local expert_mode=""
|
||||
|
||||
[[ ${EXPERT} == true ]] && expert_mode="--expert"
|
||||
|
||||
gpg --homedir "${gnupg_home_dir}" ${expert_mode} --card-edit
|
||||
}
|
||||
|
||||
#===============================================================================
|
||||
#
|
||||
# encrypt - Encrypt a clear file
|
||||
#
|
||||
#===============================================================================
|
||||
encrypt() {
|
||||
local recipient=""
|
||||
local verbose_mode=""
|
||||
reset
|
||||
rm -fr foo*
|
||||
echo CLEAR > foo.txt
|
||||
|
||||
[[ ${VERBOSE} == true ]] && verbose_mode="--verbose"
|
||||
|
||||
recipient=$(gpg --homedir "${gnupg_home_dir}" --card-status | grep "General key info" | awk '{print $NF}')
|
||||
|
||||
echo "Encrypt with recipient '${recipient}'"
|
||||
|
||||
gpg --homedir "${gnupg_home_dir}" ${verbose_mode} --encrypt --recipient "${recipient}" foo.txt
|
||||
}
|
||||
|
||||
#===============================================================================
|
||||
#
|
||||
# decrypt - Decrypt a file and compare with original clear content
|
||||
#
|
||||
#===============================================================================
|
||||
decrypt() {
|
||||
local verbose_mode=""
|
||||
|
||||
reset
|
||||
|
||||
[[ ${VERBOSE} == true ]] && verbose_mode="--verbose"
|
||||
|
||||
gpg --homedir "${gnupg_home_dir}" ${verbose_mode} --decrypt foo.txt.gpg > foo_dec.txt
|
||||
|
||||
# Check with original clear file
|
||||
diff foo.txt foo_dec.txt >/dev/null
|
||||
if [[ $? -eq 0 ]]; then
|
||||
echo "Success !"
|
||||
else
|
||||
echo "Decryption error!"
|
||||
fi
|
||||
rm -fr foo*
|
||||
}
|
||||
|
||||
#===============================================================================
|
||||
#
|
||||
# sign - Sign a file
|
||||
#
|
||||
#===============================================================================
|
||||
sign() {
|
||||
local verbose_mode=""
|
||||
|
||||
reset
|
||||
rm -fr foo*
|
||||
echo CLEAR > foo.txt
|
||||
|
||||
[[ ${VERBOSE} == true ]] && verbose_mode="--verbose"
|
||||
|
||||
gpg --homedir "${gnupg_home_dir}" ${verbose_mode} --sign foo.txt
|
||||
}
|
||||
|
||||
#===============================================================================
|
||||
#
|
||||
# verify - Verify a file signature
|
||||
#
|
||||
#===============================================================================
|
||||
verify() {
|
||||
local verbose_mode=""
|
||||
|
||||
reset
|
||||
|
||||
[[ ${VERBOSE} == true ]] && verbose_mode="--verbose"
|
||||
|
||||
gpg --homedir "${gnupg_home_dir}" ${verbose_mode} --verify foo.txt.gpg
|
||||
rm -fr foo*
|
||||
}
|
||||
|
||||
#===============================================================================
|
||||
#
|
||||
# Parsing parameters
|
||||
#
|
||||
#===============================================================================
|
||||
|
||||
if (($# < 1)); then
|
||||
help
|
||||
fi
|
||||
|
||||
while getopts ":c:evh" opt; do
|
||||
case $opt in
|
||||
|
||||
c)
|
||||
case ${OPTARG} in
|
||||
init|reset|card|encrypt|decrypt|sign|verify)
|
||||
CMD=${OPTARG}
|
||||
;;
|
||||
*)
|
||||
echo "Wrong parameter '${OPTARG}'!"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
|
||||
e) EXPERT=true ;;
|
||||
v) VERBOSE=true ;;
|
||||
h) help ;;
|
||||
|
||||
\?) echo "Unknown option: -${OPTARG}" >&2; exit 1;;
|
||||
: ) echo "Missing option argument for -${OPTARG}" >&2; exit 1;;
|
||||
* ) echo "Unimplemented option: -${OPTARG}" >&2; exit 1;;
|
||||
esac
|
||||
done
|
||||
|
||||
#===============================================================================
|
||||
#
|
||||
# Main
|
||||
#
|
||||
#===============================================================================
|
||||
|
||||
# execute the command
|
||||
${CMD}
|
@ -0,0 +1,117 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
#*****************************************************************************
|
||||
# Ledger App OpenPGP.
|
||||
# (c) 2024 Ledger SAS.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#*****************************************************************************
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from argparse import ArgumentParser, RawTextHelpFormatter, Namespace
|
||||
from gpgapp.gpgcard import GPGCard, PassWord, GPGCardExcpetion
|
||||
|
||||
# ===============================================================================
|
||||
# Parse command line options
|
||||
# ===============================================================================
|
||||
def get_argparser() -> Namespace:
|
||||
"""Parse the commandline options"""
|
||||
|
||||
parser = ArgumentParser(
|
||||
description="Backup/Restore OpenPGP App configuration",
|
||||
epilog="Keys restore is only possible with SEED mode...",
|
||||
formatter_class=RawTextHelpFormatter
|
||||
)
|
||||
parser.add_argument("--reader", type=str, default="Ledger",
|
||||
help="PCSC reader name (default is '%(default)s') or 'speculos'")
|
||||
|
||||
parser.add_argument("--slot", type=int, choices=range(1, 4), help="Select slot (1 to 3)")
|
||||
|
||||
parser.add_argument("--pinpad", action="store_true",
|
||||
help="PIN validation will be delegated to pinpad")
|
||||
parser.add_argument("--adm-pin", metavar="PIN",
|
||||
help="Admin PIN (if pinpad not used)", required="--pinpad" not in sys.argv)
|
||||
parser.add_argument("--user-pin", metavar="PIN",
|
||||
help="User PIN (if pinpad not used)", required="--pinpad" not in sys.argv)
|
||||
|
||||
parser.add_argument("--restore", action="store_true",
|
||||
help="Perform a Restore instead of Backup")
|
||||
|
||||
parser.add_argument("--file", type=str, default="gpg_backup",
|
||||
help="Backup/Restore file (default is '%(default)s')")
|
||||
|
||||
parser.add_argument("--seed-key", action="store_true",
|
||||
help="After Restore, regenerate all keys, based on seed mode")
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
# ===============================================================================
|
||||
# MAIN
|
||||
# ===============================================================================
|
||||
def entrypoint() -> None:
|
||||
"""Main function"""
|
||||
|
||||
# Arguments parsing
|
||||
# -----------------
|
||||
args = get_argparser()
|
||||
|
||||
# Arguments checking
|
||||
# ------------------
|
||||
if not args.pinpad:
|
||||
if not args.adm_pin or not args.user_pin:
|
||||
print("If 'pinpad' is not use, 'userpin' and 'admpin' must be provided.")
|
||||
sys.exit()
|
||||
|
||||
if args.restore is False:
|
||||
if Path(args.file).is_file():
|
||||
print(f"Provided backup file '{args.file}' already exist. Aborting!")
|
||||
sys.exit()
|
||||
|
||||
# Processing
|
||||
# ----------
|
||||
try:
|
||||
print(f"Connect to card '{args.reader}'...")
|
||||
gpgcard: GPGCard = GPGCard()
|
||||
gpgcard.connect(args.reader)
|
||||
|
||||
if not gpgcard.verify_pin(PassWord.PW1, args.user_pin, args.pinpad) or \
|
||||
not gpgcard.verify_pin(PassWord.PW3, args.adm_pin, args.pinpad):
|
||||
raise GPGCardExcpetion(0, "PIN not verified")
|
||||
|
||||
if args.slot:
|
||||
gpgcard.select_slot(args.slot - 1)
|
||||
|
||||
gpgcard.get_all()
|
||||
|
||||
if args.restore:
|
||||
gpgcard.restore(args.file)
|
||||
print(f"Configuration restored from file '{args.file}'.")
|
||||
|
||||
if args.seed_key:
|
||||
gpgcard.seed_key()
|
||||
|
||||
else:
|
||||
gpgcard.backup(args.file)
|
||||
print(f"Configuration saved in file '{args.file}'.")
|
||||
|
||||
gpgcard.disconnect()
|
||||
|
||||
except GPGCardExcpetion as err:
|
||||
print(f"\n### Error {err.code}: {err.message}!\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
entrypoint()
|
@ -0,0 +1,232 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#*****************************************************************************
|
||||
# Ledger App OpenPGP.
|
||||
# (c) 2024 Ledger SAS.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#*****************************************************************************
|
||||
|
||||
from enum import Enum, IntEnum
|
||||
|
||||
|
||||
KEY_TEMPLATES = {
|
||||
"rsa2048" : "010800002001",
|
||||
"rsa3072" : "010C00002001",
|
||||
# "rsa4096" : "011000002001", not supported yet
|
||||
"nistp256": "132A8648CE3D030107",
|
||||
"ed25519" : "162B06010401DA470F01",
|
||||
"cv25519" : "122B060104019755010501"
|
||||
}
|
||||
|
||||
|
||||
KEY_OPERATIONS = {
|
||||
"Export": 0x00, # Read and export a Public Key
|
||||
"Generate": 0x80, # Generate a new Asymmetric key pair
|
||||
"Read": 0x81, # Read Public Key
|
||||
}
|
||||
|
||||
|
||||
USER_SALUTATION = {
|
||||
"Male": "1",
|
||||
"Female": "2",
|
||||
}
|
||||
|
||||
|
||||
class KeyTypes(str, Enum):
|
||||
"""Key types definition
|
||||
OpenPGP Application manage four keys for cryptographic operation (PSO) plus two
|
||||
for secure channel.
|
||||
The first four keys are defined as follow:
|
||||
- One asymmetric signature private key (RSA or EC), named 'sig'
|
||||
- One asymmetric decryption private key (RSA or EC), named 'dec'
|
||||
- One asymmetric authentication private key (RSA or EC), named 'aut'
|
||||
- One symmetric decryption private key (AES), named 'sym0'
|
||||
|
||||
The 3 first asymmetric keys can be either randomly generated on-card or
|
||||
explicitly put from outside.
|
||||
The fourth is put from outside.
|
||||
"""
|
||||
|
||||
# Asymmetric Signature Private Key (RSA or EC)
|
||||
KEY_SIG = "SIG"
|
||||
# Asymmetric Decryption Private Key (RSA or EC)
|
||||
KEY_DEC = "DEC"
|
||||
# Asymmetric Authentication Private Key (RSA or EC)
|
||||
KEY_AUT = "AUT"
|
||||
# Symmetric Decryption Key (AES)
|
||||
|
||||
|
||||
class PubkeyAlgo(IntEnum):
|
||||
""" Public-Key Algorithm IDs definition """
|
||||
# https://www.rfc-editor.org/rfc/rfc4880#section-9.1
|
||||
|
||||
# RSA (Encrypt or Sign)
|
||||
RSA = 1
|
||||
# Elliptic Curve Diffie-Hellman
|
||||
ECDH = 18
|
||||
# Elliptic Curve Digital Signature Algorithm
|
||||
ECDSA = 19
|
||||
# Edwards-curve Digital Signature Algorithm
|
||||
EDDSA = 22
|
||||
|
||||
|
||||
class PassWord(IntEnum):
|
||||
""" Password type definition """
|
||||
|
||||
# USER_PIN for only one PSO:CDS command
|
||||
PW1 = 0x81
|
||||
# USER_PIN for several attempts
|
||||
PW2 = 0x82
|
||||
# Admin PIN
|
||||
PW3 = 0x83
|
||||
|
||||
|
||||
class ErrorCodes:
|
||||
""" Error codes definition """
|
||||
|
||||
err_list = {
|
||||
0x6285: "Selected file in termination state",
|
||||
0x6581: "Memory failure",
|
||||
0x6600: "Security-related issues (reserved for UIF in this application)",
|
||||
0x6700: "Wrong length (Lc and/or Le)",
|
||||
0x6881: "Logical channel not supported",
|
||||
0x6882: "Secure messaging not supported",
|
||||
0x6883: "Last command of the chain expected",
|
||||
0x6884: "Command chaining not supported",
|
||||
0x6982: "Security status not satisfied",
|
||||
0x6983: "Authentication method blocked",
|
||||
0x6984: "Data Invalid",
|
||||
0x6985: "Condition of use not satisfied",
|
||||
0x6986: "Command not allowed",
|
||||
0x6987: "Expected SM data objects missing",
|
||||
0x6988: "SM data objects incorrect",
|
||||
0x6A80: "Incorrect parameters in the data field",
|
||||
0x6A82: "File or application not found",
|
||||
0x6A86: "Incorrect P1-P2",
|
||||
0x6A88: "Referenced data not found",
|
||||
0x6B00: "Wrong parameters P1-P2",
|
||||
0x6D00: "Instruction (INS) not supported",
|
||||
0x6E00: "Class (CLA) not supported",
|
||||
0x6F00: "Unknown Error",
|
||||
0x9000: "Success",
|
||||
}
|
||||
ERR_SUCCESS = 0x9000
|
||||
ERR_SW1_VALID = 0x61
|
||||
ERR_INTERNAL = 0
|
||||
|
||||
|
||||
class DataObject(IntEnum):
|
||||
""" Data Objects definition """
|
||||
|
||||
# [Read/Write] Slot config
|
||||
CMD_SLOT_CFG = 0x01F1
|
||||
# [Read/Write] Slot selection
|
||||
CMD_SLOT_CUR = 0x01F2
|
||||
# [Read/Write] RSA Exponent
|
||||
CMD_RSA_EXP = 0x01F8
|
||||
|
||||
# [Read] Full Application identifier (AID), ISO 7816-4
|
||||
DO_AID = 0x4F
|
||||
# [Read/Write] Login data
|
||||
DO_LOGIN = 0x5E
|
||||
# [Read/Write] Uniform resource locator (URL, as defined in RFC 1738)
|
||||
DO_URL = 0x5F50
|
||||
# [Read] Historical bytes, Card service data and Card capabilities
|
||||
DO_HIST = 0x5F52
|
||||
|
||||
# [Read/Write] Optional DO for private use
|
||||
DO_PRIVATE_01 = 0x0101
|
||||
DO_PRIVATE_02 = 0x0102
|
||||
DO_PRIVATE_03 = 0x0103
|
||||
DO_PRIVATE_04 = 0x0104
|
||||
|
||||
# [Read] Cardholder Related Data
|
||||
DO_CARDHOLDER_DATA = 0x65
|
||||
# [Read/Write] Name according to ISO/IEC 7501-1)
|
||||
DO_CARD_NAME = 0x5B
|
||||
# [Read/Write] Language preferences (according to ISO 639)
|
||||
DO_CARD_LANG = 0x5F2D
|
||||
# [Read/Write] Salutation (according to ISO 5218)
|
||||
DO_CARD_SALUTATION = 0x5F35
|
||||
|
||||
# [Read/Write] Digital signature
|
||||
DO_SIG_KEY = 0xB6
|
||||
# [Read/Write] Confidentiality
|
||||
DO_DEC_KEY = 0xB8
|
||||
# [Read/Write] Authentication
|
||||
DO_AUT_KEY = 0xA4
|
||||
|
||||
# [Read] Application Related Data
|
||||
DO_APP_DATA = 0x6E
|
||||
# [Read] Extended length information (ISO 7816-4)
|
||||
DO_EXT_LEN = 0x7F66
|
||||
# [Read] Discretionary data objects
|
||||
DO_DISCRET_DATA = 0x73
|
||||
|
||||
# [Read] Extended capabilities Flag list
|
||||
DO_EXT_CAP = 0xC0
|
||||
# [Read/Write] Algorithm attributes SIGnature
|
||||
DO_SIG_ATTR = 0xC1
|
||||
# [Read/Write] Algorithm attributes DECryption
|
||||
DO_DEC_ATTR = 0xC2
|
||||
# [Read/Write] Algorithm attributes AUThentication
|
||||
DO_AUT_ATTR = 0xC3
|
||||
# [Read/Write] PW status Bytes
|
||||
DO_PW_STATUS = 0xC4
|
||||
# [Read] Fingerprints (binary, 20 bytes (dec.) each for SIG, DEC, AUT)
|
||||
DO_FINGERPRINTS = 0xC5
|
||||
# [Read] List of CA-Fingerprints (binary, 20 bytes (dec.) each for SIG, DEC, AUT)
|
||||
DO_CA_FINGERPRINTS = 0xC6
|
||||
# [Write] Fingerprint for SIGnature key, format according to RFC 4880
|
||||
DO_FINGERPRINT_WR_SIG = 0xC7
|
||||
# [Write] Fingerprint for DECryption key, format according to RFC 4880
|
||||
DO_FINGERPRINT_WR_DEC = 0xC8
|
||||
# [Write] Fingerprint for AUThentication key, format according to RFC 4880
|
||||
DO_FINGERPRINT_WR_AUT = 0xC9
|
||||
# [Write] CA-Fingerprint for SIGnature key
|
||||
DO_CA_FINGERPRINT_WR_SIG = 0xCA
|
||||
# [Write] CA-Fingerprint for DECryption key
|
||||
DO_CA_FINGERPRINT_WR_DEC = 0xCB
|
||||
# [Write] CA-Fingerprint for AUThentication key
|
||||
DO_CA_FINGERPRINT_WR_AUT = 0xCC
|
||||
# [Read] List of generation dates. 4 bytes, Big Endian each for SIG, DEC, AUT
|
||||
DO_KEY_DATES = 0xCD
|
||||
|
||||
# [Write] Generation date/time of SIGnature key (Big Endian, according to RFC 4880)
|
||||
DO_DATES_WR_SIG = 0xCE
|
||||
# [Write] Generation date/time of DECryption key (Big Endian, according to RFC 4880)
|
||||
DO_DATES_WR_DEC = 0xCF
|
||||
# [Write] Generation date/time of AUThentication key (Big Endian, according to RFC 4880)
|
||||
DO_DATES_WR_AUT = 0xD0
|
||||
|
||||
# [Read] Security support template
|
||||
DO_SEC_TEMPL = 0x7A
|
||||
# [Read] Digital signature counter
|
||||
DO_SIG_COUNT = 0x93
|
||||
|
||||
# [Write] Resetting Code
|
||||
DO_RESET_CODE = 0xD3
|
||||
# [Read/Write] User Interaction Flag (UIF) for PSO:CDS
|
||||
DO_UIF_SIG = 0xD6
|
||||
# [Read/Write] User Interaction Flag (UIF) for PSO:DEC
|
||||
DO_UIF_DEC = 0xD7
|
||||
# [Read/Write] User Interaction Flag (UIF) for PSO:AUT
|
||||
DO_UIF_AUT = 0xD8
|
||||
# [Read/Write] Cardholder certificate (each for AUT, DEC and SIG)
|
||||
DO_CERT = 0x7F21
|
||||
|
||||
# [Read/Write] Asymmetric Key Pair
|
||||
DO_PUB_KEY = 0x7F49
|
||||
|
||||
# [Read] General Feature management
|
||||
DO_GEN_FEATURES = 0x7F74
|
@ -1,15 +0,0 @@
|
||||
# Copyright 2017 Cedric Mesnil <cslashm@gmail.com>, Ledger SAS
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
@ -1,25 +0,0 @@
|
||||
# Copyright 2017 Cedric Mesnil <cslashm@gmail.com>, Ledger SAS
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
from gpgcard import GPGCard
|
||||
|
||||
gpgcard = GPGCard()
|
||||
gpgcard.connect("pcsc:Ledger")
|
||||
gpgcard.get_all()
|
||||
|
||||
|
||||
gpgcard.verify_pin(0x81, "123456")
|
||||
gpgcard.verify_pin(0x83, "12345678")
|
||||
gpgcard.backup("backup_card.pickle")
|
@ -1,850 +0,0 @@
|
||||
# Copyright 2017 Cedric Mesnil <cslashm@gmail.com>, Ledger SAS
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
|
||||
try:
|
||||
from ledgerblue.comm import getDongle
|
||||
from ledgerblue.commException import CommException
|
||||
except :
|
||||
pass
|
||||
import binascii
|
||||
|
||||
from smartcard.System import readers
|
||||
import sys
|
||||
import datetime
|
||||
import pickle
|
||||
|
||||
# decode level 0 of tlv|tlv|tlv
|
||||
# return dico {t: v, t:v, ...}
|
||||
#tlv: hexstring
|
||||
|
||||
def decode_tlv(tlv) :
|
||||
tags = {}
|
||||
while len(tlv) :
|
||||
o = 0
|
||||
l = 0
|
||||
if (tlv[0] & 0x1F) == 0x1F:
|
||||
t = (tlv[0]<<8)|tlv[1]
|
||||
o = 2
|
||||
else:
|
||||
t = tlv[0]
|
||||
o = 1
|
||||
l = tlv[o]
|
||||
if l & 0x80 :
|
||||
if (l&0x7f) == 1:
|
||||
l = tlv[o+1]
|
||||
o += 2
|
||||
if (l&0x7f) == 2:
|
||||
l = (tlv[o+1]<<8)|tlv[o+2]
|
||||
o += 3
|
||||
else:
|
||||
o += 1
|
||||
v = tlv[o:o+l]
|
||||
tags[t] = v
|
||||
tlv = tlv[o+l:]
|
||||
return tags
|
||||
|
||||
class GPGCardExcpetion(Exception):
|
||||
pass
|
||||
|
||||
class GPGCard() :
|
||||
def __init__(self):
|
||||
self.reset()
|
||||
self.log = False
|
||||
|
||||
def reset(self):
|
||||
#token info
|
||||
self.AID = b''
|
||||
self.aid = b''
|
||||
self.ext_length = b''
|
||||
self.ext_capabilities = b''
|
||||
self.histo_bytes = b''
|
||||
self.PW_status = b''
|
||||
|
||||
#user info
|
||||
self.login = b''
|
||||
self.url = b''
|
||||
self.name = b''
|
||||
self.sex = b''
|
||||
self.lang = b''
|
||||
|
||||
#keys info
|
||||
self.cardholder_cert = b''
|
||||
self.sig_attribute = b''
|
||||
self.dec_attribute = b''
|
||||
self.aut_attribute = b''
|
||||
self.sig_fingerprints = b''
|
||||
self.dec_fingerprints = b''
|
||||
self.aut_fingerprints = b''
|
||||
self.sig_CA_fingerprints = b''
|
||||
self.dec_CA_fingerprints = b''
|
||||
self.aut_CA_fingerprints = b''
|
||||
self.sig_date = b''
|
||||
self.dec_date = b''
|
||||
self.aut_date = b''
|
||||
self.cert_aut = b''
|
||||
self.cert_dec = b''
|
||||
self.cert_sig = b''
|
||||
self.UIF_SIG = b''
|
||||
self.UIF_DEC = b''
|
||||
self.UIF_AUT = b''
|
||||
self.digital_counter = b''
|
||||
|
||||
#private info
|
||||
self.private_01 = b''
|
||||
self.private_02 = b''
|
||||
self.private_03 = b''
|
||||
|
||||
#keys
|
||||
self.sig_key = b''
|
||||
self.dec_key = b''
|
||||
self.aut_key = b''
|
||||
|
||||
|
||||
def connect(self, device):
|
||||
self.token = None
|
||||
if device.startswith("ledger:"):
|
||||
self.token = getDongle(True)
|
||||
self.exchange = self._exchange_ledger
|
||||
self.disconnect = self._disconnect_ledger
|
||||
elif device.startswith("pcsc:"):
|
||||
allreaders = readers()
|
||||
for r in allreaders:
|
||||
rname = str(r)
|
||||
#print('try: %s : %s'%(rname,device[5:]))
|
||||
if rname.startswith(device[5:]):
|
||||
r.createConnection()
|
||||
self.token = r
|
||||
self.connection = r.createConnection()
|
||||
self.connection.connect()
|
||||
self.exchange = self._exchange_pcsc
|
||||
self.disconnect = self._disconnect_pcsc
|
||||
else:
|
||||
#print("No")
|
||||
pass
|
||||
if not self.token:
|
||||
print("No token")
|
||||
|
||||
|
||||
|
||||
|
||||
### APDU interface ###
|
||||
def _exchange_ledger(self,cmd,data=None, sw=0x9000):
|
||||
resp = b''
|
||||
cond = True
|
||||
while cond:
|
||||
try:
|
||||
resp = resp + self.token.exchange(cmd,300)
|
||||
sw = 0x9000
|
||||
cond = False
|
||||
except CommException as e:
|
||||
if (e.data) :
|
||||
resp = resp + e.data
|
||||
sw = e.sw
|
||||
if (sw&0xFF00) == 0x6100 :
|
||||
cmd = binascii.unhexlify("00C00000%.02x"%(sw&0xFF))
|
||||
else:
|
||||
cond = False
|
||||
return resp,sw
|
||||
|
||||
def log_apdu(self,l):
|
||||
self.log = l
|
||||
|
||||
def alog(self, m,dt,sw=0):
|
||||
if self.log:
|
||||
print("%s %.04x %s"%(m,sw,''.join(["%.02x"%b for b in dt])))
|
||||
|
||||
def _exchange_pcsc(self,apdu, data=None, sw_expected=0x9000, sw_mask=0xFFFF):
|
||||
if data:
|
||||
data = [x for x in data]
|
||||
apdu = [x for x in apdu]
|
||||
#send
|
||||
if data:
|
||||
while len(data) > 0xFE:
|
||||
apdux = apdu[0:5]+[0xfe]+data[0:0xFE]
|
||||
apdux[0] |= 0x10
|
||||
self.alog('send', apdux)
|
||||
resp, sw1, sw2 = self.connection.transmit(apdux)
|
||||
sw = (sw1<<8)|sw2
|
||||
self.alog('recv',resp,sw)
|
||||
if sw != 0x9000:
|
||||
return resp,sw
|
||||
data = data[0xFE:]
|
||||
apdu = apdu+[len(data)]+data
|
||||
self.alog('send', apdu)
|
||||
resp, sw1, sw2 = self.connection.transmit(apdu)
|
||||
sw = (sw1<<8)|sw2
|
||||
self.alog('recv', resp, sw)
|
||||
|
||||
#receive
|
||||
while sw1==0x61:
|
||||
apdu = binascii.unhexlify(b"00c00000%.02x"%sw2)
|
||||
apdu = [x for x in apdu]
|
||||
self.alog('send', apdu)
|
||||
resp2, sw1, sw2 = self.connection.transmit(apdu)
|
||||
sw = (sw1<<8)|sw2
|
||||
self.alog('recv', resp2, sw)
|
||||
resp = resp + resp2
|
||||
resp = bytes(resp)
|
||||
sw = (sw1<<8)|sw2
|
||||
if sw&sw_mask == sw_expected:
|
||||
return resp,sw
|
||||
raise GPGCardExcpetion(binascii.hexlify(resp), "%.04x"%sw)
|
||||
|
||||
def _disconnect_ledger(self):
|
||||
return self.token.close()
|
||||
|
||||
def _disconnect_pcsc(self):
|
||||
r = self.connection.disconnect()
|
||||
#self.connection.releaseContext()
|
||||
return r
|
||||
|
||||
def select(self):
|
||||
apdu = binascii.unhexlify(b"00A4040006D27600012401")
|
||||
return self.exchange(apdu)
|
||||
|
||||
def activate(self):
|
||||
apdu = binascii.unhexlify(b"00440000")
|
||||
return self.exchange(apdu)
|
||||
|
||||
def terminate(self):
|
||||
apdu = binascii.unhexlify(b"00E60000")
|
||||
return self.exchange(apdu)
|
||||
|
||||
def get_log(self):
|
||||
apdu = binascii.unhexlify(b"00040000")
|
||||
return self.exchange(apdu)
|
||||
|
||||
def get_data(self,tag):
|
||||
apdu = binascii.unhexlify(b"00CA%.04x00"%tag)
|
||||
return self.exchange(apdu)
|
||||
|
||||
def put_data(self,tag,value):
|
||||
return self.exchange(binascii.unhexlify(b"00DA%.04x"%tag), value)
|
||||
|
||||
def verify(self,id,value, pinpad=False):
|
||||
if pinpad:
|
||||
apdu = binascii.unhexlify(b"EF2000%.02x00"%id)
|
||||
else:
|
||||
apdu = binascii.unhexlify(b"002000%.02x%.02x"%(id,len(value)))+value
|
||||
return self.exchange(apdu)
|
||||
|
||||
def change_reference_data(self,id,value,new_value):
|
||||
lc = len(value)+len(new_value)
|
||||
apdu = binascii.unhexlify(b"002400%.02x%.02x"%(id,lc))+value+new_value
|
||||
return self.exchange(apdu)
|
||||
|
||||
def reset_retry_counter(self,RC,new_value):
|
||||
if len(RC)==0:
|
||||
p1 = 2
|
||||
else:
|
||||
p1 = 0
|
||||
lc = len(RC)+len(new_value)
|
||||
apdu = binascii.unhexlify(b"002C%02x81%.02x"%(p1,lc))+RC+new_value
|
||||
return self.exchange(apdu)
|
||||
|
||||
def generate_asym_key_pair(self, mode, key):
|
||||
apdu = binascii.unhexlify(b"0047%02x0002%.04x"%(mode,key))
|
||||
return self.exchange(apdu)
|
||||
|
||||
### API interfaces ###
|
||||
def get_all(self, with_key=False):
|
||||
self.reset()
|
||||
|
||||
self.slot,sw = self.get_data(0x01F2)
|
||||
self.AID,sw = self.get_data(0x4f)
|
||||
self.login ,sw = self.get_data(0x5e)
|
||||
self.url,sw = self.get_data(0x5f50)
|
||||
self.histo_bytes,sw = self.get_data(0x5f52)
|
||||
|
||||
cardholder,sw = self.get_data(0x65)
|
||||
tags = decode_tlv(cardholder)
|
||||
if 0x5b in tags:
|
||||
self.name = tags[0x5b]
|
||||
if 0x5f35 in tags:
|
||||
self.sex = tags[0x5f35]
|
||||
if 0x5f35 in tags:
|
||||
self.lang = tags[0x5f2d]
|
||||
|
||||
application_data,sw = self.get_data(0x6E)
|
||||
tags = decode_tlv(application_data)
|
||||
if 0x7f66 in tags:
|
||||
self.ext_length = tags[0x7f66]
|
||||
if 0x73 in tags:
|
||||
dicretionary_data = tags[0x73]
|
||||
tags = decode_tlv(dicretionary_data)
|
||||
if 0xc0 in tags:
|
||||
self.ext_capabilities = tags[0xC0]
|
||||
|
||||
if 0xc4 in tags:
|
||||
self.PW_status = tags[0xC4]
|
||||
|
||||
if 0xC1 in tags:
|
||||
self.sig_attribute = tags[0xC1]
|
||||
if 0xC2 in tags:
|
||||
self.dec_attribute = tags[0xC2]
|
||||
if 0xC3 in tags:
|
||||
self.aut_attribute = tags[0xC3]
|
||||
if 0xC5 in tags:
|
||||
fingerprints = tags[0xC5]
|
||||
self.sig_fingerprints = fingerprints[0:20]
|
||||
self.dec_fingerprints = fingerprints[20:40]
|
||||
self.aut_fingerprints = fingerprints[40:60]
|
||||
if 0xC6 in tags:
|
||||
fingerprints = tags[0xC6]
|
||||
self.sig_CA_fingerprints = fingerprints[0:20]
|
||||
self.dec_CA_fingerprints = fingerprints[20:40]
|
||||
self.aut_CA_fingerprints = fingerprints[40:60]
|
||||
if 0xcd in tags:
|
||||
dates = tags[0xCD]
|
||||
self.sig_date = dates[0:4]
|
||||
self.dec_date = dates[4:8]
|
||||
self.aut_date = dates[8:12]
|
||||
|
||||
self.cardholder_cert = self.get_data(0x7f21)
|
||||
|
||||
self.UIF_SIG,sw = self.get_data(0xD6)
|
||||
self.UIF_DEC,sw = self.get_data(0xD7)
|
||||
self.UIF_AUT,sw = self.get_data(0xD8)
|
||||
|
||||
sec_template,sw = self.get_data(0x7A)
|
||||
tags = decode_tlv(sec_template)
|
||||
if 0x93 in tags:
|
||||
self.digital_counter = tags[0x93]
|
||||
|
||||
self.private_01,sw = self.get_data(0x0101)
|
||||
self.private_02,sw = self.get_data(0x0102)
|
||||
self.private_03,sw = self.get_data(0x0103)
|
||||
self.private_04,sw = self.get_data(0x0104)
|
||||
|
||||
if with_key:
|
||||
self.sig_key,sw = self.get_data(0x00B6)
|
||||
self.dec_key,sw = self.get_data(0x00B8)
|
||||
self.aut_key,sw = self.get_data(0x00A4)
|
||||
|
||||
|
||||
return True
|
||||
|
||||
def set_all(self):
|
||||
|
||||
self.put_data(0x4f, self.AID[10:14])
|
||||
self.put_data(0x0101, self.private_01)
|
||||
self.put_data(0x0102, self.private_02)
|
||||
self.put_data(0x0103, self.private_03)
|
||||
self.put_data(0x0104, self.private_04)
|
||||
|
||||
self.put_data(0x5b, self.name)
|
||||
self.put_data(0x5e, self.login)
|
||||
self.put_data(0x5f2d, self.lang)
|
||||
self.put_data(0x5f35, self.sex)
|
||||
self.put_data(0x5f50, self.url)
|
||||
|
||||
self.put_data(0xc1, self.sig_attribute)
|
||||
self.put_data(0xc2, self.dec_attribute)
|
||||
self.put_data(0xc3, self.aut_attribute)
|
||||
|
||||
self.put_data(0xc4, self.PW_status)
|
||||
|
||||
self.put_data(0xc7, self.sig_fingerprints)
|
||||
self.put_data(0xc8, self.dec_fingerprints)
|
||||
self.put_data(0xc9, self.aut_fingerprints)
|
||||
self.put_data(0xca, self.sig_CA_fingerprints)
|
||||
self.put_data(0xcb, self.dec_CA_fingerprints)
|
||||
self.put_data(0xcc, self.aut_CA_fingerprints)
|
||||
self.put_data(0xce, self.sig_date)
|
||||
self.put_data(0xcf, self.dec_date)
|
||||
self.put_data(0xd0, self.aut_date)
|
||||
#self.put_data(0x7f21, self.cardholder_cert)
|
||||
|
||||
self.put_data(0xd6, self.UIF_SIG)
|
||||
self.put_data(0xd7, self.UIF_DEC)
|
||||
self.put_data(0xd8, self.UIF_AUT)
|
||||
|
||||
if len(self.sig_key):
|
||||
self.put_data(0x00B6, self.sig_key)
|
||||
if len(self.dec_key):
|
||||
self.put_data(0x00B8, self.dec_key)
|
||||
if len(self.aut_key):
|
||||
self.put_data(0x00A4, self.aut_key)
|
||||
return True
|
||||
|
||||
|
||||
def _backup_file_name(self,file_name):
|
||||
return file_name #file_name+"_slot%d"%(self.slot[0]+1)+".pickle"
|
||||
|
||||
def backup(self, file_name, with_key=False):
|
||||
self.get_all(with_key)
|
||||
file_name = self._backup_file_name(file_name)
|
||||
f = open(file_name,mode='w+b')
|
||||
pickle.dump(
|
||||
(self.AID,
|
||||
self.private_01, self.private_02, self.private_03, self.private_04,
|
||||
self.name, self.login, self.sex, self.url,
|
||||
self.sig_attribute, self.dec_attribute, self.aut_attribute,
|
||||
self.PW_status,
|
||||
self.sig_fingerprints, self.dec_fingerprints, self.aut_fingerprints,
|
||||
self.sig_CA_fingerprints, self.dec_CA_fingerprints, self.aut_CA_fingerprints,
|
||||
self.sig_date, self.dec_date, self.aut_date,
|
||||
self.cardholder_cert,
|
||||
self.UIF_SIG, self.UIF_DEC, self.UIF_AUT,
|
||||
self.sig_key, self.dec_key, self.aut_key),
|
||||
f, 2)
|
||||
return True
|
||||
|
||||
|
||||
def restore(self, file_name):
|
||||
file_name = self._backup_file_name(file_name)
|
||||
f = open(file_name,mode='r+b')
|
||||
(self.AID,
|
||||
self.private_01, self.private_02, self.private_03, self.private_04,
|
||||
self.name, self.login, self.sex, self.url,
|
||||
self.sig_attribute, self.dec_attribute, self.aut_attribute,
|
||||
self.PW_status,
|
||||
self.sig_fingerprints, self.dec_fingerprints, self.aut_fingerprints,
|
||||
self.sig_CA_fingerprints, self.dec_CA_fingerprints, self.aut_CA_fingerprints,
|
||||
self.sig_date, self.dec_date, self.aut_date,
|
||||
self.cardholder_cert,
|
||||
self.UIF_SIG, self.UIF_DEC, self.UIF_AUT,
|
||||
self.sig_key, self.dec_key, self.aut_key) = pickle.load(f)
|
||||
self.set_all()
|
||||
return True
|
||||
|
||||
def seed_key(self):
|
||||
apdu = binascii.unhexlify(b"0047800102B600")
|
||||
self.exchange(apdu)
|
||||
apdu = binascii.unhexlify(b"0047800102B800")
|
||||
self.exchange(apdu)
|
||||
apdu = binascii.unhexlify(b"0047800102A400")
|
||||
self.exchange(apdu)
|
||||
|
||||
|
||||
def decode_AID(self):
|
||||
return {
|
||||
'AID': ('AID' , "%x"%int.from_bytes(self.AID,'big')),
|
||||
'RID': ('RID' , "%x"%int.from_bytes(self.AID[0:5],'big')),
|
||||
'APP': ('application' , "%.02x"%self.AID[5]),
|
||||
'VER': ('version' , "%.02x.%.02x"%(self.AID[6], self.AID[7])),
|
||||
'MAN': ('manufacturer' , "%x"%int.from_bytes(self.AID[8:10],'big')),
|
||||
'SER': ('serial' , "%x"%int.from_bytes(self.AID[10:14],'big'))
|
||||
}
|
||||
|
||||
def decode_histo(self):
|
||||
return {
|
||||
'HIST': ('historical bytes', binascii.hexlify(self.histo_bytes))
|
||||
}
|
||||
|
||||
def decode_extlength(self):
|
||||
if self.ext_length:
|
||||
return {
|
||||
'CMD': ('Max command length' , "%d" %((self.ext_length[2]<<8)|self.ext_length[3])),
|
||||
'RESP':( 'Max response length' , "%d" %((self.ext_length[6]<<8)|self.ext_length[7]))
|
||||
}
|
||||
else:
|
||||
return {
|
||||
'CMD': ('Max command length' , "unspecified"),
|
||||
'RESP':( 'Max response length' ,"unspecified"),
|
||||
}
|
||||
|
||||
def decode_capabilities(self):
|
||||
d = {}
|
||||
b1 = self.ext_capabilities[0]
|
||||
if b1&0x80 :
|
||||
if self.ext_capabilities[1] == 1:
|
||||
d['SM'] = ('Secure Messaging', "yes: 128 bits")
|
||||
elif self.ext_capabilities[1] == 2:
|
||||
d['SM'] = ('Secure Messaging', "yes: 256 bits")
|
||||
else:
|
||||
d['SM'] = ('Secure Messaging', "yes: ?? bits")
|
||||
else:
|
||||
d['SM'] = ('Secure Messaging', "no")
|
||||
|
||||
if b1&0x40 :
|
||||
d['CHAL'] = ('Get Challenge', "yes")
|
||||
else:
|
||||
d['CHAL'] = ('Get Challenge', "no")
|
||||
|
||||
if b1&0x20 :
|
||||
d['KEY'] = ('Key import', "yes")
|
||||
else:
|
||||
d['KEY'] = ('Key import', "no")
|
||||
|
||||
if b1&0x10 :
|
||||
d['PWS'] = ('PW status changeable', "yes")
|
||||
else:
|
||||
d['PWS'] = ('PW status changeable', "no")
|
||||
|
||||
if b1&0x08 :
|
||||
d['PDO'] = ('Private DOs', "yes")
|
||||
else:
|
||||
d['PDO'] = ('Private DOs', "no")
|
||||
|
||||
if b1&0x04 :
|
||||
d['ATTR'] = ('Algo attributes changeable', "yes")
|
||||
else:
|
||||
dd['ATTR'] = ('Algo attributes changeable', "no")
|
||||
|
||||
if b1&0x02 :
|
||||
d['PSO'] = ('PSO:DEC support AES', "yes")
|
||||
else:
|
||||
d['PSO'] = ('PSO:DEC support AES', "no")
|
||||
|
||||
|
||||
d['CHAL_MAX'] = ('Max GET_CHALLENGE length',
|
||||
"%d"% ((self.ext_capabilities[2]<<8)|self.ext_capabilities[3]))
|
||||
d['CERT_MAX'] = ('Max Cert length',
|
||||
"%d"% ((self.ext_capabilities[4]<<8)|self.ext_capabilities[5]))
|
||||
d['PDO_MAX'] = ('Max special DO length',
|
||||
"%d"% ((self.ext_capabilities[6]<<8)|self.ext_capabilities[7]))
|
||||
if self.ext_capabilities[8] :
|
||||
d['PIN2'] = ('PIN 2 format supported', "yes")
|
||||
else:
|
||||
d['PIN2'] = ('PIN 2 format supported',"no")
|
||||
|
||||
return d
|
||||
|
||||
def decode_pws(self):
|
||||
d = {}
|
||||
if self.PW_status[0]==0:
|
||||
d['ONCE'] = ('PW1 valid for several CDS', 'yes')
|
||||
elif self.PW_status[0]==1:
|
||||
d['ONCE'] = ('PW1 valid for several CDS', 'no')
|
||||
else:
|
||||
d['ONCE'] = ('PW1 valid for several CDS', 'unknown (%d)'%self.PW_status[0])
|
||||
|
||||
if self.PW_status[1] & 0x80:
|
||||
fmt = "Format-2"
|
||||
else:
|
||||
fmt = "UTF-8"
|
||||
pwlen = self.PW_status[1] & 0x7f
|
||||
d['PW1'] = ("PW1 format", "%s : %d bytes"%(fmt,pwlen))
|
||||
|
||||
if self.PW_status[2] & 0x80:
|
||||
fmt = "Format-2"
|
||||
else:
|
||||
fmt = "UTF-8"
|
||||
pwlen = self.PW_status[2] & 0x7f
|
||||
d['RC'] = ("RC format", "%s : %d bytes"%(fmt,pwlen))
|
||||
|
||||
if self.PW_status[3] & 0x80:
|
||||
fmt = "Format-2"
|
||||
else:
|
||||
fmt = "UTF-8"
|
||||
pwlen = self.PW_status[3] & 0x7f
|
||||
d['PW3'] = ("PW3 format", "%s : %d bytes"%(fmt,pwlen))
|
||||
|
||||
d['CNT1'] = ('PW1 counter', "%x"%self.PW_status[4])
|
||||
d['CNTRC'] =('RC counter', "%x"%self.PW_status[5])
|
||||
d['CNT3'] = ('PW3 counter', "%x"%self.PW_status[6])
|
||||
|
||||
return d
|
||||
|
||||
#slot
|
||||
def select_slot(self, slot):
|
||||
""" Args:
|
||||
slot (int) : slot id (1 to MAX) to select
|
||||
"""
|
||||
self.put_data( 0x01F2, (slot-1).to_bytes(1,'big'))
|
||||
|
||||
#USER Info
|
||||
def set_serial(self, ser):
|
||||
ser=binascii.unhexlify(ser)
|
||||
|
||||
self.AID = self.AID[0:10]+ser
|
||||
self.put_data(0x4f, self.AID[10:14])
|
||||
|
||||
# internals are always store as byres, get/set automatically convert from/to
|
||||
def set_name(self,name):
|
||||
""" Args:
|
||||
name (str) : utf8 string
|
||||
"""
|
||||
self.name = name.encode('utf-8')
|
||||
self.put_data( 0x5b, self.name)
|
||||
|
||||
def get_name(self):
|
||||
return self.name.decode('utf-8')
|
||||
|
||||
def set_login(self,login):
|
||||
""" Args:
|
||||
login (str) : utf8 string
|
||||
"""
|
||||
self.login = login.encode('utf-8')
|
||||
self.put_data( 0x5e, self.login)
|
||||
|
||||
def get_login(self):
|
||||
return self.login.decode('utf-8')
|
||||
|
||||
def set_url(self,url):
|
||||
""" Args:
|
||||
url (str) : utf8 string
|
||||
"""
|
||||
self.url = url.encode('utf-8')
|
||||
self.put_data(0x5f50, self.url)
|
||||
|
||||
def get_url(self):
|
||||
return self.url.decode('utf-8')
|
||||
|
||||
def set_sex(self,sex):
|
||||
""" Args:
|
||||
sex (str) : ascii string ('9', '1', '2')
|
||||
"""
|
||||
self.sex = sex.encode('utf-8')
|
||||
self.put_data(0x5f35, self.sex)
|
||||
|
||||
def get_sex(self):
|
||||
return self.sex.decode('utf-8')
|
||||
|
||||
def set_lang(self,lang):
|
||||
""" Args:
|
||||
lang (str) : utf8 string
|
||||
"""
|
||||
self.lang = lang.encode('utf-8')
|
||||
self.put_data(0x5f2d, self.lang)
|
||||
|
||||
def get_lang(self):
|
||||
return self.lang.decode('utf-8')
|
||||
|
||||
|
||||
#PINs
|
||||
def verify_pin(self,id,value, pinpad=False):
|
||||
""" Args:
|
||||
id (int) : 0x81, 0x82, ox83
|
||||
value (str) : ascii string
|
||||
"""
|
||||
value = value.encode('ascii')
|
||||
resp,sw = self.verify(id,value, pinpad)
|
||||
return sw == 0x9000
|
||||
|
||||
def change_pin(self, id, value,new_value):
|
||||
""" Args:
|
||||
id (int) : 0x81, ox83
|
||||
value (str) : ascii string
|
||||
"""
|
||||
value = value.encode('ascii')
|
||||
new_value = new_value.encode('ascii')
|
||||
resp,sw = self.change_reference_data(id,value,new_value)
|
||||
return sw == 0x9000
|
||||
|
||||
def change_RC(self,new_value):
|
||||
""" Args:
|
||||
id (int) : 0x81, ox83
|
||||
value (str) : ascii string
|
||||
"""
|
||||
new_value = new_value.encode('ascii')
|
||||
resp,sw = self.put_data(0xd3,new_value)
|
||||
return sw == 0x9000
|
||||
|
||||
def reset_PW1(self,RC,new_value):
|
||||
""" Args:
|
||||
id (int) : 0x81, ox83
|
||||
value (str) : ascii string
|
||||
"""
|
||||
new_value = new_value.encode('ascii')
|
||||
RC = RC.encode('ascii')
|
||||
resp,sw = self.reset_retry_counter(RC,new_value)
|
||||
return sw == 0x9000
|
||||
|
||||
#keys
|
||||
def get_key_uif(self,key):
|
||||
"""
|
||||
Returns: (int) 0,1,2,256(not supported)
|
||||
"""
|
||||
uif = None
|
||||
if key=='sig':
|
||||
uif = self.UIF_SIG
|
||||
if key=='aut':
|
||||
uif = self.UIF_DEC
|
||||
if key=='dec':
|
||||
uif = self.UIF_AUT
|
||||
|
||||
if uif:
|
||||
uif = int.from_bytes(uif,'big')
|
||||
else:
|
||||
uif = 256
|
||||
return uif
|
||||
|
||||
def get_key_fingerprints(self, key):
|
||||
"""
|
||||
Returns: (str) fingerprints hex string
|
||||
"""
|
||||
fprints = None
|
||||
if key=='sig':
|
||||
fprints = self.sig_fingerprints
|
||||
if key=='aut':
|
||||
fprints = self.aut_fingerprints
|
||||
if key=='dec':
|
||||
fprints = self.dec_fingerprints
|
||||
if fprints:
|
||||
fprints = binascii.hexlify(fprints)
|
||||
else:
|
||||
fprint = '-'
|
||||
return fprints.decode('ascii')
|
||||
|
||||
def set_key_fingerprints(self, key, fprints):
|
||||
|
||||
fprints = binascii.unhexlify(fprints)
|
||||
if key=='sig':
|
||||
self.sig_fingerprints = fprints
|
||||
self.put_data(0xc7, fprints)
|
||||
if key=='aut':
|
||||
self.aut_fingerprints = fprints
|
||||
self.put_data(0xc9, fprints)
|
||||
if key=='dec':
|
||||
self.dec_fingerprints = fprints
|
||||
self.put_data(0xc8, fprints)
|
||||
|
||||
|
||||
def get_key_CA_fingerprints(self, key):
|
||||
"""
|
||||
Returns: (str) CA fingerprints hex string
|
||||
"""
|
||||
fprints = None
|
||||
if key=='sig':
|
||||
fprints = self.sig_CA_fingerprints
|
||||
if key=='aut':
|
||||
fprints = self.aut_CA_fingerprints
|
||||
if key=='dec':
|
||||
fprints = self.dec_CA_fingerprints
|
||||
if fprints:
|
||||
fprints = binascii.hexlify(fprints)
|
||||
else:
|
||||
fprint = b'-'
|
||||
return fprints.decode('ascii')
|
||||
|
||||
def get_key_date(self, key):
|
||||
"""
|
||||
Returns: (str) date
|
||||
"""
|
||||
fdate = None
|
||||
if key=='sig':
|
||||
fdate = self.sig_date
|
||||
if key=='aut':
|
||||
fdate = self.aut_date
|
||||
if key=='dec':
|
||||
fdate = self.dec_date
|
||||
if fdate:
|
||||
fdate = datetime.datetime.fromtimestamp(int.from_bytes(fdate,'big')).isoformat(' ')
|
||||
else:
|
||||
fprint = b'-'.decode('ascii')
|
||||
return fdate
|
||||
|
||||
|
||||
def get_key_attribute(self, key):
|
||||
"""
|
||||
for RSA:
|
||||
{'id': (int) 0x01,
|
||||
'nsize': (int)
|
||||
'esize' (int)
|
||||
'format': (int)
|
||||
}
|
||||
|
||||
for ECC:
|
||||
|
||||
{'id': (int) 0x18|0x19,
|
||||
'OID': (bytes)
|
||||
}
|
||||
|
||||
Args:
|
||||
key: (str) 'sig' | 'aut', 'dec'
|
||||
"""
|
||||
|
||||
attributes = None
|
||||
if key=='sig':
|
||||
attributes = self.sig_attribute
|
||||
if key=='dec':
|
||||
attributes = self.dec_attribute
|
||||
if key=='aut':
|
||||
attributes = self.aut_attribute
|
||||
if not attributes:
|
||||
return None
|
||||
if len(attributes) == 0:
|
||||
return None
|
||||
|
||||
if attributes[0] == 0x01:
|
||||
return {
|
||||
'id': 1,
|
||||
'nsize': (attributes[1]<<8) | attributes[2],
|
||||
'esize': (attributes[3]<<8) | attributes[4],
|
||||
'format': attributes[5]
|
||||
}
|
||||
if attributes[0] == 18 or attributes[0] == 19 :
|
||||
return {
|
||||
'id': attributes[0] ,
|
||||
'oid': attributes[1:]
|
||||
}
|
||||
print ("NONE: %s"%binascii.hexlify(attributes))
|
||||
return None
|
||||
|
||||
def set_template(self, sig, dec, aut):
|
||||
"""
|
||||
See get_template
|
||||
"""
|
||||
if (sig):
|
||||
self.put_data(0x00C1, binascii.unhexlify(sig))
|
||||
if dec:
|
||||
self.put_data(0x00C2, binascii.unhexlify(dec))
|
||||
if aut:
|
||||
self.put_data(0x00C3, binascii.unhexlify(aut))
|
||||
pass
|
||||
|
||||
|
||||
def asymmetric_key(self, op, key) :
|
||||
"""
|
||||
Args:
|
||||
op: (int) 0x80 generate, 0x81 read pub, 0x82 read pub&priv
|
||||
key: (str) 'sig' | 'aut', 'dec'
|
||||
|
||||
Returns:
|
||||
for RSA:
|
||||
{'id': (int) 0x01,
|
||||
'n': (bytes)
|
||||
'e' (bytes)
|
||||
'd': (bytes)
|
||||
}
|
||||
|
||||
for ECC:
|
||||
|
||||
{'id': (int) 0x18|0x19,
|
||||
'OID': (bytes)
|
||||
}
|
||||
|
||||
"""
|
||||
attributes = None
|
||||
if key=='sig':
|
||||
attributes = self.sig_attribute
|
||||
key = 0xb600
|
||||
if key=='dec':
|
||||
attributes = self.dec_attribute
|
||||
key = 0xb800
|
||||
if key=='aut':
|
||||
attributes = self.aut_attribute
|
||||
key = 0xa400
|
||||
if not attributes:
|
||||
return None
|
||||
if len(attributes) == 0:
|
||||
return None
|
||||
resp,sw = self.generate_asym_key_pair(op,key)
|
||||
if sw != 0x9000:
|
||||
return None
|
||||
resp,sw = self.generate_asym_key_pair(0x82,key)
|
||||
tags = decode_tlv(resp)
|
||||
tags = decode_tlv(tags[0x7f49])
|
||||
if attributes[0] == 0x01:
|
||||
return {
|
||||
'id': 1,
|
||||
'n': tags[0x81],
|
||||
'e': tags[0x82],
|
||||
'd': tags[0x98],
|
||||
}
|
||||
|
@ -1,156 +0,0 @@
|
||||
# Copyright 2018 Cedric Mesnil <cslashm@gmail.com>, Ledger SAS
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
import sys
|
||||
import argparse
|
||||
import binascii
|
||||
|
||||
from .gpgcard import GPGCard
|
||||
|
||||
def get_argparser():
|
||||
parser = argparse.ArgumentParser(epilog="""
|
||||
reset, backup, restore are always executed in THIS order.
|
||||
|
||||
Template identifiers are ed2559, cv25519, rsa2048, rsa3072, rsa4096.
|
||||
"""
|
||||
)
|
||||
parser.add_argument('--adm-pin', metavar='PIN', help='Administrative PIN, if pinpad not used')
|
||||
parser.add_argument('--backup', help='Perfom a full backup except the key', action='store_true')
|
||||
parser.add_argument('--backup-keys', help='Perfom keys encrypted backup', action='store_true')
|
||||
parser.add_argument('--file', help='basckup/restore file', type=str, default='gpg_backup')
|
||||
parser.add_argument('--pinpad', help='PIN validation will be deledated to pinpad', action='store_true')
|
||||
parser.add_argument('--reader', help='PCSC reader', type=str, default='pcsc:Ledger')
|
||||
parser.add_argument('--reset', help='Reset the application. All data are erased', action='store_true')
|
||||
parser.add_argument('--restore', help='Perfom a full restore except the key', action='store_true')
|
||||
parser.add_argument('--set-serial', metavar='SERIAL', help='set the four serial bytes')
|
||||
parser.add_argument('--set-templates', metavar='SIG:DEC:AUT', help='sig:dec:aut templates identifier')
|
||||
parser.add_argument('--set-fingerprints', metavar='SIG:DEC:AUT', help='sig:dec:aut fingerprints, 20 bytes each in hexa')
|
||||
parser.add_argument('--seed-key', help='Regenerate all keys, based on seed mode', action='store_true')
|
||||
parser.add_argument('--slot', metavar='SLOT', help='slot to backup', type=int, default=1)
|
||||
parser.add_argument('--user-pin', metavar='PIN', help='User PIN, if pinpad not used'),
|
||||
parser.add_argument('--apdu', help='Log APDU exchange', action='store_true')
|
||||
return parser
|
||||
|
||||
def banner():
|
||||
print(
|
||||
"""
|
||||
GPG Ledger Admin Tool v0.1.
|
||||
Copyright 2018 Cedric Mesnil <cslashm@gmail.com>, Ledger SAS
|
||||
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def error(msg) :
|
||||
print("Error: ")
|
||||
print(" "+msg)
|
||||
sys.exit()
|
||||
|
||||
banner()
|
||||
|
||||
args = get_argparser().parse_args()
|
||||
|
||||
if args.backup and args.restore:
|
||||
error('Only one backup or restore must be specified')
|
||||
|
||||
|
||||
if not args.pinpad:
|
||||
if not args.adm_pin or not args.user_pin:
|
||||
error('If pinpad is not use, userpin and admpin must be provided')
|
||||
|
||||
|
||||
try:
|
||||
|
||||
print("Connect to card %s..."%args.reader, end='', flush=True)
|
||||
gpgcard = GPGCard()
|
||||
if args.apdu:
|
||||
gpgcard.log_apdu(args.apdu)
|
||||
gpgcard.connect(args.reader)
|
||||
print("OK")
|
||||
|
||||
print("Verfify PINs...", end='', flush=True)
|
||||
if args.pinpad:
|
||||
if not gpgcard.verify_pin(0x82, "", True) or not gpgcard.verify_pin(0x83, "", True):
|
||||
error("PIN not verified")
|
||||
else:
|
||||
if not gpgcard.verify_pin(0x82, args.user_pin) or not gpgcard.verify_pin(0x83, args.adm_pin):
|
||||
error("PIN not verified")
|
||||
print("OK")
|
||||
|
||||
print("Select slot %d..."%args.slot, end='', flush=True)
|
||||
gpgcard.select_slot(args.slot)
|
||||
print("OK")
|
||||
|
||||
if args.reset:
|
||||
print("Reset application...", end='', flush=True)
|
||||
gpgcard.terminate()
|
||||
gpgcard.activate()
|
||||
print("OK")
|
||||
|
||||
print("Get card info...", end='', flush=True)
|
||||
gpgcard.get_all()
|
||||
print("OK", flush=True)
|
||||
|
||||
if args.backup:
|
||||
print("Backup application...", end='', flush=True)
|
||||
if not gpgcard.backup(args.file, args.backup_keys):
|
||||
error("NOK")
|
||||
print("OK")
|
||||
|
||||
if args.restore:
|
||||
print("Restore application...", end='', flush=True)
|
||||
if not gpgcard.restore(args.file):
|
||||
error("NOK")
|
||||
print("OK", flush=True)
|
||||
|
||||
if args.set_templates:
|
||||
print("Set template...", end='', flush=True)
|
||||
templates= {
|
||||
'rsa2048' : "010800002001",
|
||||
'rsa3072' : "010C00002001",
|
||||
'rsa4096' : "011000002001",
|
||||
'nistp256' : "132A8648CE3D030107",
|
||||
'ed255519' : "162B06010401DA470F01",
|
||||
'cv25519' : "122B060104019755010501"
|
||||
}
|
||||
sig,dec,aut = args.set_templates.split(":")
|
||||
gpgcard.set_template(templates[sig],templates[dec],templates[aut])
|
||||
print("OK", flush=True)
|
||||
|
||||
if args.seed_key:
|
||||
print("Seed Key...", end='', flush=True)
|
||||
gpgcard.seed_key();
|
||||
print("OK", flush=True)
|
||||
|
||||
if args.set_fingerprints:
|
||||
print("Set fingerprints...", end='', flush=True)
|
||||
sig,dec,aut = args.set_fingerprints.split(":")
|
||||
if sig:
|
||||
gpgcard.set_key_fingerprints("sig", sig)
|
||||
if dec:
|
||||
gpgcard.set_key_fingerprints("dec", dec)
|
||||
if aut:
|
||||
gpgcard.set_key_fingerprints("aut", aut)
|
||||
print("OK", flush=True)
|
||||
|
||||
if args.set_serial:
|
||||
print("Set serial...", end='', flush=True)
|
||||
if len(args.set_serial) != 8 :
|
||||
error('Serial must be a 4 bytes hexa string value (8 characters)')
|
||||
serial = binascii.unhexlify(args.set_serial)
|
||||
gpgcard.set_serial(args.set_serial)
|
||||
print("OK", flush=True)
|
||||
except Exception as e:
|
||||
error(str(e))
|
@ -1,25 +0,0 @@
|
||||
# Copyright 2017 Cedric Mesnil <cslashm@gmail.com>, Ledger SAS
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
from gpgcard import GPGCard
|
||||
|
||||
gpgcard = GPGCard()
|
||||
gpgcard.connect("pcsc:Ledger")
|
||||
gpgcard.get_all()
|
||||
|
||||
|
||||
gpgcard.verify_pin(0x81, "123456")
|
||||
gpgcard.verify_pin(0x83, "12345678")
|
||||
gpgcard.restore("backup_card.pickle", True)
|
@ -0,0 +1,397 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
#*****************************************************************************
|
||||
# Ledger App OpenPGP.
|
||||
# (c) 2024 Ledger SAS.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#*****************************************************************************
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from argparse import ArgumentParser, RawTextHelpFormatter, Namespace
|
||||
from gpgapp.gpgcard import GPGCard, GPGCardExcpetion
|
||||
from gpgapp.gpgcmd import ErrorCodes, KeyTypes, PassWord
|
||||
from gpgapp.gpgcmd import KEY_OPERATIONS, KEY_TEMPLATES, USER_SALUTATION
|
||||
|
||||
|
||||
# ===============================================================================
|
||||
# Parse command line options
|
||||
# ===============================================================================
|
||||
def get_argparser() -> Namespace:
|
||||
"""Parse the commandline options"""
|
||||
|
||||
parser = ArgumentParser(
|
||||
description="Manage OpenPGP App on Ledger device",
|
||||
formatter_class=RawTextHelpFormatter
|
||||
)
|
||||
parser.add_argument("--info", action="store_true",
|
||||
help="Get and display card information")
|
||||
parser.add_argument("--reader", type=str, default="Ledger",
|
||||
help="PCSC reader name (default is '%(default)s') or 'speculos'")
|
||||
|
||||
parser.add_argument("--apdu", action="store_true", help="Log APDU exchange")
|
||||
parser.add_argument("--slot", type=int, choices=range(1, 4), help="Select slot (1 to 3)")
|
||||
parser.add_argument("--reset", action="store_true",
|
||||
help="Reset the application (all data will be erased)")
|
||||
|
||||
parser.add_argument("--pinpad", action="store_true",
|
||||
help="PIN validation will be delegated to pinpad")
|
||||
parser.add_argument("--adm-pin", metavar="PIN",
|
||||
help="Admin PIN (if pinpad not used)", required="--pinpad" not in sys.argv)
|
||||
parser.add_argument("--user-pin", metavar="PIN",
|
||||
help="User PIN (if pinpad not used)", required="--pinpad" not in sys.argv)
|
||||
parser.add_argument("--new-user-pin", metavar="PIN",
|
||||
help="Change User PIN")
|
||||
parser.add_argument("--new-adm-pin", metavar="PIN",
|
||||
help="Change Admin PIN")
|
||||
group = parser.add_mutually_exclusive_group()
|
||||
group.add_argument("--reset-code", help="Update 'PW1 Resetting Code'")
|
||||
group.add_argument("--reset-pw1", help="Reset the User PIN")
|
||||
|
||||
parser.add_argument("--serial", help="Update the 'serial' data (4 bytes)")
|
||||
parser.add_argument("--salutation",choices=list(USER_SALUTATION), help="Update 'salutation' data")
|
||||
parser.add_argument("--name", help="Update 'name' data")
|
||||
parser.add_argument("--url", help="Update 'url' data")
|
||||
parser.add_argument("--login",help="Update 'login' data")
|
||||
parser.add_argument("--lang", help="Update 'lang' data")
|
||||
|
||||
parser.add_argument("--key-type", type=KeyTypes, choices=[k.value for k in KeyTypes],
|
||||
help="Select key type SIG:DEC:AUT (default is all)")
|
||||
|
||||
parser.add_argument("--key-action",choices=list(KEY_OPERATIONS),
|
||||
help="Generate key pair or Read public key")
|
||||
parser.add_argument("--set-fingerprints", metavar="SIG:DEC:AUT",
|
||||
help="Set fingerprints for selected 'key-type'\n" + \
|
||||
"If 'key-type' is not specified, set for all keys (SIG:DEC:AUT)\n" + \
|
||||
"Each fingerprint is 20 hex bytes long")
|
||||
parser.add_argument("--set-templates", metavar="SIG:DEC:AUT",
|
||||
help="Set template identifier for selected 'key-type'\n" + \
|
||||
"If 'key-type' is not specified, set for all keys (SIG:DEC:AUT)\n" + \
|
||||
f"Valid values are {', '.join(list(KEY_TEMPLATES))}")
|
||||
parser.add_argument("--seed-key", action="store_true",
|
||||
help="Regenerate all keys, based on seed mode")
|
||||
|
||||
parser.add_argument("--file", type=str, default="pubkey",
|
||||
help="Public Key export file (default is '%(default)s')")
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
# ===============================================================================
|
||||
# Error handler
|
||||
# ===============================================================================
|
||||
def error(code: int, msg: str) -> None:
|
||||
"""Print error message and exit
|
||||
|
||||
Args:
|
||||
msg (str): Message to display
|
||||
"""
|
||||
|
||||
scode = f" {code:x}" if code else ""
|
||||
if not msg:
|
||||
if code in ErrorCodes.err_list:
|
||||
msg = ErrorCodes.err_list[code]
|
||||
print(f"\n### Error{scode}: {msg}\n")
|
||||
sys.exit()
|
||||
|
||||
|
||||
# ===============================================================================
|
||||
# PIN codes verification
|
||||
# ===============================================================================
|
||||
def verify_pins(gpgcard: GPGCard, user_pin: str, adm_pin: str, pinpad: bool) -> None:
|
||||
"""Verify the pin codes
|
||||
|
||||
Args:
|
||||
gpgcard (GPGCard): smartcard object
|
||||
user_pin (str): User pin code
|
||||
adm_pin (str): Admin pin code
|
||||
pinpad (bool): Indicates to use pinpad
|
||||
"""
|
||||
|
||||
print("Verify PINs...")
|
||||
if not gpgcard.verify_pin(PassWord.PW1, user_pin, pinpad) or \
|
||||
not gpgcard.verify_pin(PassWord.PW2, user_pin, pinpad) or \
|
||||
not gpgcard.verify_pin(PassWord.PW3, adm_pin, pinpad):
|
||||
error(ErrorCodes.ERR_INTERNAL, "PIN not verified")
|
||||
|
||||
|
||||
# ===============================================================================
|
||||
# Reset the Application
|
||||
# ===============================================================================
|
||||
def reset_app(gpgcard: GPGCard) -> None:
|
||||
"""Reset Application and re-init
|
||||
|
||||
Args:
|
||||
gpgcard (GPGCard): smartcard object
|
||||
"""
|
||||
|
||||
print("Reset application...")
|
||||
gpgcard.terminate()
|
||||
gpgcard.activate()
|
||||
print(" -> OK")
|
||||
|
||||
|
||||
# ===============================================================================
|
||||
# Retrieve the OpenPGP Card information
|
||||
# ===============================================================================
|
||||
def get_info(gpgcard: GPGCard, display: bool=True) -> None:
|
||||
"""Retrieve and display Card information
|
||||
|
||||
Args:
|
||||
gpgcard (GPGCard): smartcard object
|
||||
display (bool): Print Card info
|
||||
"""
|
||||
|
||||
print("Get card info...")
|
||||
gpgcard.get_all()
|
||||
|
||||
if not display:
|
||||
return
|
||||
|
||||
line = "=" * 15
|
||||
print(f"{line} Application Identifier {line}")
|
||||
for k, v in gpgcard.decode_AID().items():
|
||||
if k == "AID":
|
||||
print(f" # {k:20s}: {v}")
|
||||
else:
|
||||
print(f" - {k:18s}: {v}")
|
||||
print(f"{line} Historical Bytes {line}")
|
||||
for k, v in gpgcard.decode_histo().items():
|
||||
print(f" - {k:20s}: {v}")
|
||||
print(f"{line} Max Extended Length {line}")
|
||||
for k, v in gpgcard.decode_extlength().items():
|
||||
print(f" - {k:20s}: {v}")
|
||||
print(f"{line} PIN Info {line}")
|
||||
for k, v in gpgcard.decode_pws().items():
|
||||
print(f" - {k:20s}: {v}")
|
||||
print(f"{line} Extended Capabilities {line}")
|
||||
for k, v in gpgcard.decode_ext_capabilities().items():
|
||||
print(f" - {k:20s}: {v}")
|
||||
print(f"{line} Hardware Features {line}")
|
||||
for k, v in gpgcard.decode_hardware().items():
|
||||
print(f" - {k:20s}: {v}")
|
||||
print(f"{line} User Info {line}")
|
||||
print(f" - {'Name':20s}: {gpgcard.get_name()}")
|
||||
print(f" - {'Login':20s}: {gpgcard.get_login()}")
|
||||
print(f" - {'URL':20s}: {gpgcard.get_url()}")
|
||||
print(f" - {'Salutation':20s}: {gpgcard.get_salutation()}")
|
||||
print(f" - {'Lang':20s}: {gpgcard.get_lang()}")
|
||||
print(f"{line} Slots Info {line}")
|
||||
for k, v in gpgcard.decode_slot().items():
|
||||
print(f" - {k:20s}: {v}")
|
||||
print(f"{line} Keys Info {line}")
|
||||
print(f" - {'CDS counter':20s}: {gpgcard.get_sig_count()}")
|
||||
print(f" - {'RSA Pub Exponent':20s}: 0x{gpgcard.get_rsa_pub_exp():06x}")
|
||||
|
||||
for key in [k.value for k in KeyTypes]:
|
||||
print(f" # {key}:")
|
||||
print(f" - {'UIF':18s}: {gpgcard.decode_key_uif(key)}")
|
||||
print(f" - {'Fingerprint':18s}: {gpgcard.get_key_fingerprint(key)}")
|
||||
print(f" - {'CA fingerprint':18s}: {gpgcard.get_key_CA_fingerprint(key)}")
|
||||
print(f" - {'Creation date':18s}: {gpgcard.get_key_date(key)}")
|
||||
print(f" - {'Attribute':18s}: {gpgcard.decode_attributes(key)}")
|
||||
print(f" - {'Certificate':18s}: {gpgcard.get_key_cert(key)}")
|
||||
print(" - Key:")
|
||||
for k, v in gpgcard.decode_key(key).items():
|
||||
print(f" * {k:16s}: {v}")
|
||||
|
||||
|
||||
# ===============================================================================
|
||||
# Set fingerprints
|
||||
# ===============================================================================
|
||||
def set_fingerprints(gpgcard: GPGCard, fingerprints: str, key_type: KeyTypes | None = None) -> None:
|
||||
"""Set Key template
|
||||
|
||||
Args:
|
||||
gpgcard (GPGCard): smartcard object
|
||||
fingerprints (str): SIG, DEC, AUT fingerprints separated by ':'
|
||||
key_type (KeyTypes): Key type selected
|
||||
"""
|
||||
|
||||
d = {}
|
||||
if key_type is None:
|
||||
# Consider all keys fingerprints are given
|
||||
try:
|
||||
d[KeyTypes.KEY_SIG], d[KeyTypes.KEY_DEC], d[KeyTypes.KEY_AUT] = fingerprints.split(":")
|
||||
except ValueError as err:
|
||||
raise GPGCardExcpetion(0, f"Wrong fingerprints arguments: {err}") from err
|
||||
|
||||
else:
|
||||
# a key_type is specified, using only this fingerprint
|
||||
d[key_type] = fingerprints
|
||||
|
||||
for k, v in d.items():
|
||||
print(f"Set fingerprints for '{k}' Key...")
|
||||
gpgcard.set_key_fingerprint(k, bytes.fromhex(v))
|
||||
|
||||
|
||||
# ===============================================================================
|
||||
# Set Key Templates
|
||||
# ===============================================================================
|
||||
def set_templates(gpgcard: GPGCard, templates: str, key_type: KeyTypes | None = None) -> None:
|
||||
"""Set Key template
|
||||
|
||||
Args:
|
||||
gpgcard (GPGCard): smartcard object
|
||||
templates (str): SIG, DEC, AUT template separated by ':'
|
||||
key_type (KeyTypes): Key type selected
|
||||
"""
|
||||
|
||||
d = {}
|
||||
if key_type is None:
|
||||
# Consider all keys template are given
|
||||
try:
|
||||
d[KeyTypes.KEY_SIG], d[KeyTypes.KEY_DEC], d[KeyTypes.KEY_AUT] = templates.split(":")
|
||||
except ValueError as err:
|
||||
raise GPGCardExcpetion(0, f"Wrong templates arguments: {err}") from err
|
||||
else:
|
||||
# a key_type is specified, using only this template
|
||||
d[key_type] = templates
|
||||
|
||||
for _, v in d.items():
|
||||
if v not in KEY_TEMPLATES:
|
||||
raise GPGCardExcpetion(0, f"Invalid template: {v}")
|
||||
|
||||
for k, v in d.items():
|
||||
print(f"Set template {v} for '{k}' Key...")
|
||||
gpgcard.set_template(k, v)
|
||||
|
||||
|
||||
# ===============================================================================
|
||||
# Handle Asymmetric keys
|
||||
# ===============================================================================
|
||||
def handle_key(gpgcard: GPGCard, action: str, key_type: KeyTypes, file: str = "") -> None:
|
||||
"""Generate Key pair and/or Read Public key
|
||||
|
||||
Args:
|
||||
gpgcard (GPGCard): smartcard object
|
||||
action (str): Generate or Read
|
||||
key_type (KeyTypes): Key type selected
|
||||
file (str): Public key export file
|
||||
"""
|
||||
|
||||
if action not in KEY_OPERATIONS:
|
||||
raise GPGCardExcpetion(0, f"Invalid operation: {action}")
|
||||
|
||||
key_list = [key_type] if key_type else list(KeyTypes)
|
||||
for key in key_list:
|
||||
print(f"{action} '{key}' Key...")
|
||||
key_action = "Read" if action == "Export" else action
|
||||
pubkey = gpgcard.asymmetric_key(key, key_action)
|
||||
if action == "Export":
|
||||
if len(key_list) > 1:
|
||||
filename = key + "_" + file
|
||||
else:
|
||||
filename = file
|
||||
path = Path(filename)
|
||||
if path.suffix == "":
|
||||
filename += ".pem"
|
||||
gpgcard.export_pub_key(pubkey, filename)
|
||||
else:
|
||||
for k, v in pubkey.items():
|
||||
print(f" - {k:13s}: {v}")
|
||||
|
||||
|
||||
# ===============================================================================
|
||||
# MAIN
|
||||
# ===============================================================================
|
||||
def entrypoint() -> None:
|
||||
"""Main function"""
|
||||
|
||||
# Arguments parsing
|
||||
# -----------------
|
||||
args = get_argparser()
|
||||
|
||||
# Arguments checking
|
||||
# ------------------
|
||||
if not args.pinpad:
|
||||
if not args.adm_pin or not args.user_pin:
|
||||
error(ErrorCodes.ERR_INTERNAL,
|
||||
"If 'pinpad' is not use, 'userpin' and 'admpin' must be provided")
|
||||
|
||||
if args.serial and len(args.serial) != 8 :
|
||||
error(ErrorCodes.ERR_INTERNAL,
|
||||
"Serial must be a 4 bytes hex string value (8 characters)")
|
||||
|
||||
if args.reset_code and len(args.reset_code) != 8:
|
||||
error(ErrorCodes.ERR_INTERNAL,
|
||||
"Reset Code must be a 4 bytes hex string value (8 characters)")
|
||||
|
||||
if args.key_action == "Export" and not args.file:
|
||||
error(ErrorCodes.ERR_INTERNAL, "Provide a file to export public key")
|
||||
|
||||
# Processing
|
||||
# ----------
|
||||
try:
|
||||
print(f"Connect to card '{args.reader}'...")
|
||||
gpgcard: GPGCard = GPGCard()
|
||||
gpgcard.log_apdu(args.apdu)
|
||||
gpgcard.connect(args.reader)
|
||||
|
||||
verify_pins(gpgcard, args.user_pin, args.adm_pin, args.pinpad)
|
||||
|
||||
if args.slot:
|
||||
gpgcard.select_slot(args.slot - 1)
|
||||
|
||||
if args.salutation:
|
||||
gpgcard.set_salutation(args.salutation)
|
||||
if args.name:
|
||||
gpgcard.set_name(args.name)
|
||||
if args.url:
|
||||
gpgcard.set_url(args.url)
|
||||
if args.login:
|
||||
gpgcard.set_login(args.login)
|
||||
if args.lang:
|
||||
gpgcard.set_lang(args.lang)
|
||||
|
||||
if args.new_user_pin:
|
||||
gpgcard.change_pin(PassWord.PW1, args.user_pin, args.new_user_pin)
|
||||
if args.new_adm_pin:
|
||||
gpgcard.change_pin(PassWord.PW3, args.adm_pin, args.new_adm_pin)
|
||||
if args.reset_pw1:
|
||||
# Reset the User PIN with Resetting Code
|
||||
gpgcard.reset_PW1(args.reset_code, args.reset_pw1)
|
||||
elif args.reset_code:
|
||||
# Use the Resetting code to set the value
|
||||
gpgcard.set_RC(args.reset_code)
|
||||
|
||||
get_info(gpgcard, args.info)
|
||||
|
||||
if args.reset:
|
||||
reset_app(gpgcard)
|
||||
|
||||
if args.set_templates:
|
||||
set_templates(gpgcard, args.set_templates, args.key_type)
|
||||
|
||||
if args.seed_key:
|
||||
gpgcard.seed_key()
|
||||
|
||||
if args.set_fingerprints:
|
||||
set_fingerprints(gpgcard, args.set_fingerprints, args.key_type)
|
||||
|
||||
if args.serial:
|
||||
gpgcard.set_serial(args.serial)
|
||||
|
||||
if args.key_action:
|
||||
handle_key(gpgcard, args.key_action, args.key_type, args.file)
|
||||
|
||||
gpgcard.disconnect()
|
||||
|
||||
except GPGCardExcpetion as err:
|
||||
error(err.code, err.message)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
entrypoint()
|
@ -0,0 +1,2 @@
|
||||
pycryptodome
|
||||
ledgercomm
|
@ -0,0 +1,28 @@
|
||||
[tool:pytest]
|
||||
addopts = --strict-markers
|
||||
|
||||
[pylint]
|
||||
disable = C0114, # missing-module-docstring
|
||||
C0115, # missing-class-docstring
|
||||
C0116, # missing-function-docstring
|
||||
C0103, # invalid-name
|
||||
C0302, # too-many-lines
|
||||
R0801, # duplicate-code
|
||||
R0902, # too-many-instances
|
||||
R0903, # too-few-public-methods
|
||||
R0904, # too-many-public-methods
|
||||
R0912, # too-many-branches
|
||||
R0913, # too-many-arguments
|
||||
R0914, # too-many-statements
|
||||
R0915 # too-many-local-variables
|
||||
max-line-length=110
|
||||
extension-pkg-whitelist=hid
|
||||
|
||||
[pycodestyle]
|
||||
max-line-length = 100
|
||||
|
||||
[mypy-hid.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-pytest.*]
|
||||
ignore_missing_imports = True
|
@ -1,67 +1,95 @@
|
||||
/* Copyright 2017 Cedric Mesnil <cslashm@gmail.com>, Ledger SAS
|
||||
/*****************************************************************************
|
||||
* Ledger App OpenPGP.
|
||||
* (c) 2024 Ledger SAS.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*****************************************************************************/
|
||||
|
||||
#include "os.h"
|
||||
#include "cx.h"
|
||||
#include "gpg_types.h"
|
||||
#include "gpg_api.h"
|
||||
#include "gpg_vars.h"
|
||||
#include "cx_errors.h"
|
||||
|
||||
/**
|
||||
* Generate a Random Number
|
||||
*
|
||||
* @return Status Word
|
||||
*
|
||||
*/
|
||||
int gpg_apdu_get_challenge() {
|
||||
unsigned int olen, hlen;
|
||||
unsigned int olen;
|
||||
cx_err_t error = CX_INTERNAL_ERROR;
|
||||
unsigned char Sr[64];
|
||||
|
||||
if ((G_gpg_vstate.io_p1 & 0x80) == 0x80) {
|
||||
olen = G_gpg_vstate.io_p2;
|
||||
} else {
|
||||
olen = G_gpg_vstate.io_le;
|
||||
}
|
||||
if (olen == 0 || olen > GPG_EXT_CHALLENGE_LENTH) {
|
||||
THROW(SW_WRONG_LENGTH);
|
||||
return SW_WRONG_LENGTH;
|
||||
}
|
||||
switch (G_gpg_vstate.io_p1) {
|
||||
case CHALLENGE_NOMINAL:
|
||||
case PRIME_MODE:
|
||||
olen = G_gpg_vstate.io_le;
|
||||
break;
|
||||
case SEEDED_MODE:
|
||||
olen = G_gpg_vstate.io_p2;
|
||||
break;
|
||||
default:
|
||||
return SW_WRONG_P1P2;
|
||||
}
|
||||
if (olen == 0 || olen > GPG_EXT_CHALLENGE_LENTH) {
|
||||
return SW_WRONG_LENGTH;
|
||||
}
|
||||
|
||||
if ((G_gpg_vstate.io_p1 & 0x82) == 0x82) {
|
||||
unsigned int path[2];
|
||||
unsigned char chain[32];
|
||||
unsigned char Sr[32];
|
||||
if (G_gpg_vstate.io_p1 == SEEDED_MODE) {
|
||||
// Ledger Add-on: Seeded random
|
||||
unsigned int path[2];
|
||||
unsigned char chain[32] = {0};
|
||||
|
||||
os_memset(chain, 0, 32);
|
||||
path[0] = 0x80475047;
|
||||
path[1] = 0x0F0F0F0F;
|
||||
os_perso_derive_node_bip32(CX_CURVE_SECP256K1, path, 2, Sr, chain);
|
||||
chain[0] = 'r';
|
||||
chain[1] = 'n';
|
||||
chain[2] = 'd';
|
||||
explicit_bzero(chain, 32);
|
||||
path[0] = 0x80475047;
|
||||
path[1] = 0x0F0F0F0F;
|
||||
CX_CHECK(os_derive_bip32_no_throw(CX_CURVE_SECP256K1, path, 2, Sr, chain));
|
||||
chain[0] = 'r';
|
||||
chain[1] = 'n';
|
||||
chain[2] = 'd';
|
||||
|
||||
cx_sha256_init(&G_gpg_vstate.work.md.sha256);
|
||||
cx_hash((cx_hash_t *)&G_gpg_vstate.work.md.sha256, 0, Sr, 32, NULL, 0);
|
||||
cx_hash((cx_hash_t *)&G_gpg_vstate.work.md.sha256, 0, chain, 3, NULL, 0);
|
||||
hlen = cx_hash((cx_hash_t *)&G_gpg_vstate.work.md.sha256, CX_LAST, G_gpg_vstate.work.io_buffer,
|
||||
G_gpg_vstate.io_length, G_gpg_vstate.work.io_buffer, 32);
|
||||
cx_sha256_init(&G_gpg_vstate.work.md.sha256);
|
||||
CX_CHECK(cx_hash_no_throw((cx_hash_t *) &G_gpg_vstate.work.md.sha256, 0, Sr, 32, NULL, 0));
|
||||
CX_CHECK(
|
||||
cx_hash_no_throw((cx_hash_t *) &G_gpg_vstate.work.md.sha256, 0, chain, 3, NULL, 0));
|
||||
CX_CHECK(cx_hash_no_throw((cx_hash_t *) &G_gpg_vstate.work.md.sha256,
|
||||
CX_LAST,
|
||||
G_gpg_vstate.work.io_buffer,
|
||||
G_gpg_vstate.io_length,
|
||||
G_gpg_vstate.work.io_buffer,
|
||||
32));
|
||||
CX_CHECK(cx_sha3_xof_init_no_throw(&G_gpg_vstate.work.md.sha3, 256, olen));
|
||||
CX_CHECK(cx_hash_no_throw((cx_hash_t *) &G_gpg_vstate.work.md.sha3,
|
||||
CX_LAST,
|
||||
G_gpg_vstate.work.io_buffer,
|
||||
32,
|
||||
G_gpg_vstate.work.io_buffer,
|
||||
olen));
|
||||
} else {
|
||||
cx_rng(G_gpg_vstate.work.io_buffer, olen);
|
||||
error = CX_OK;
|
||||
}
|
||||
|
||||
cx_sha3_xof_init(&G_gpg_vstate.work.md.sha3, 256, olen);
|
||||
cx_hash((cx_hash_t *)&G_gpg_vstate.work.md.sha3, CX_LAST, G_gpg_vstate.work.io_buffer, hlen,
|
||||
G_gpg_vstate.work.io_buffer, olen);
|
||||
} else {
|
||||
cx_rng(G_gpg_vstate.work.io_buffer, olen);
|
||||
}
|
||||
if (G_gpg_vstate.io_p1 == PRIME_MODE) {
|
||||
// Ledger Add-on: Prime random
|
||||
CX_CHECK(cx_math_next_prime_no_throw(G_gpg_vstate.work.io_buffer, olen));
|
||||
}
|
||||
|
||||
if ((G_gpg_vstate.io_p1 & 0x81) == 0x81) {
|
||||
cx_math_next_prime(G_gpg_vstate.work.io_buffer, olen);
|
||||
}
|
||||
gpg_io_discard(0);
|
||||
gpg_io_inserted(olen);
|
||||
return SW_OK;
|
||||
end:
|
||||
explicit_bzero(&Sr, sizeof(Sr));
|
||||
if (error != CX_OK) {
|
||||
return error;
|
||||
}
|
||||
gpg_io_discard(0);
|
||||
gpg_io_inserted(olen);
|
||||
return SW_OK;
|
||||
}
|
||||
|
@ -1,429 +1,442 @@
|
||||
/* Copyright 2017 Cedric Mesnil <cslashm@gmail.com>, Ledger SAS
|
||||
/*****************************************************************************
|
||||
* Ledger App OpenPGP.
|
||||
* (c) 2024 Ledger SAS.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*****************************************************************************/
|
||||
|
||||
#include "os.h"
|
||||
#include "cx.h"
|
||||
#include "gpg_types.h"
|
||||
#include "gpg_api.h"
|
||||
#include "gpg_vars.h"
|
||||
|
||||
void gpg_check_access_ins() {
|
||||
unsigned int ref;
|
||||
|
||||
ref = (G_gpg_vstate.io_p1 << 8) | G_gpg_vstate.io_p2;
|
||||
|
||||
switch (G_gpg_vstate.io_ins) {
|
||||
case INS_EXIT:
|
||||
if (gpg_pin_is_verified(PIN_ID_PW2)) {
|
||||
return;
|
||||
}
|
||||
break;
|
||||
#ifdef GPG_LOG
|
||||
#warning GPG_LOG activated
|
||||
case INS_GET_LOG:
|
||||
return;
|
||||
#endif
|
||||
|
||||
case INS_SELECT:
|
||||
return;
|
||||
case INS_GET_DATA:
|
||||
case INS_GET_NEXT_DATA:
|
||||
return;
|
||||
|
||||
case INS_VERIFY:
|
||||
return;
|
||||
|
||||
case INS_CHANGE_REFERENCE_DATA:
|
||||
return;
|
||||
|
||||
case INS_RESET_RETRY_COUNTER:
|
||||
if (gpg_pin_is_verified(PIN_ID_PW3) || gpg_pin_is_verified(PIN_ID_RC)) {
|
||||
return;
|
||||
}
|
||||
break;
|
||||
|
||||
case INS_PUT_DATA:
|
||||
case INS_PUT_DATA_ODD:
|
||||
return;
|
||||
|
||||
case INS_GEN_ASYM_KEYPAIR:
|
||||
if (G_gpg_vstate.io_p1 == 0x81) {
|
||||
return;
|
||||
}
|
||||
if (gpg_pin_is_verified(PIN_ID_PW3)) {
|
||||
return;
|
||||
}
|
||||
break;
|
||||
|
||||
case INS_MSE:
|
||||
return;
|
||||
|
||||
case INS_PSO:
|
||||
if ((ref == 0x9e9a) && gpg_pin_is_verified(PIN_ID_PW1)) {
|
||||
// pso:sign
|
||||
if (N_gpg_pstate->PW_status[0] == 0) {
|
||||
gpg_pin_set_verified(PIN_ID_PW1, 0);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (((ref == 0x8086) || (ref == 0x8680)) && gpg_pin_is_verified(PIN_ID_PW2)) {
|
||||
// pso:dec/enc
|
||||
return;
|
||||
}
|
||||
break;
|
||||
|
||||
case INS_INTERNAL_AUTHENTICATE:
|
||||
if (gpg_pin_is_verified(PIN_ID_PW2)) {
|
||||
return;
|
||||
}
|
||||
break;
|
||||
|
||||
case INS_GET_CHALLENGE:
|
||||
return;
|
||||
|
||||
case INS_TERMINATE_DF:
|
||||
if (gpg_pin_is_verified(PIN_ID_PW3)) {
|
||||
return;
|
||||
/**
|
||||
* Check INS access condition
|
||||
* Verify if the corresponding PW is verified
|
||||
*
|
||||
* @return Status Word
|
||||
*
|
||||
*/
|
||||
static int gpg_check_access_ins() {
|
||||
int sw = SW_UNKNOWN;
|
||||
|
||||
switch (G_gpg_vstate.io_ins) {
|
||||
case INS_EXIT:
|
||||
if (gpg_pin_is_verified(PIN_ID_PW2)) {
|
||||
sw = SW_OK;
|
||||
}
|
||||
break;
|
||||
|
||||
case INS_RESET_RETRY_COUNTER:
|
||||
if (gpg_pin_is_verified(PIN_ID_PW3) || gpg_pin_is_verified(PIN_ID_RC)) {
|
||||
sw = SW_OK;
|
||||
}
|
||||
break;
|
||||
|
||||
case INS_GEN_ASYM_KEYPAIR:
|
||||
if ((G_gpg_vstate.io_p1 == ((READ_ASYM_KEY >> 8) & 0xFF)) ||
|
||||
(gpg_pin_is_verified(PIN_ID_PW3))) {
|
||||
sw = SW_OK;
|
||||
}
|
||||
break;
|
||||
|
||||
case INS_PSO:
|
||||
if ((G_gpg_vstate.io_p1p2 == PSO_CDS) && gpg_pin_is_verified(PIN_ID_PW1)) {
|
||||
// pso:sign
|
||||
if (N_gpg_pstate->PW_status[0] == 0) {
|
||||
gpg_pin_set_verified(PIN_ID_PW1, 0);
|
||||
}
|
||||
sw = SW_OK;
|
||||
break;
|
||||
}
|
||||
if (((G_gpg_vstate.io_p1p2 == PSO_DEC) || (G_gpg_vstate.io_p1p2 == PSO_ENC)) &&
|
||||
gpg_pin_is_verified(PIN_ID_PW2)) {
|
||||
// pso:dec/enc
|
||||
sw = SW_OK;
|
||||
}
|
||||
break;
|
||||
|
||||
case INS_INTERNAL_AUTHENTICATE:
|
||||
if (gpg_pin_is_verified(PIN_ID_PW2)) {
|
||||
sw = SW_OK;
|
||||
}
|
||||
break;
|
||||
|
||||
case INS_TERMINATE_DF:
|
||||
if (gpg_pin_is_verified(PIN_ID_PW3)) {
|
||||
sw = SW_OK;
|
||||
}
|
||||
break;
|
||||
|
||||
#ifdef GPG_LOG
|
||||
#warning GPG_LOG activated
|
||||
case INS_GET_LOG:
|
||||
#endif
|
||||
case INS_SELECT:
|
||||
case INS_GET_DATA:
|
||||
case INS_GET_NEXT_DATA:
|
||||
case INS_VERIFY:
|
||||
case INS_CHANGE_REFERENCE_DATA:
|
||||
case INS_PUT_DATA:
|
||||
case INS_PUT_DATA_ODD:
|
||||
case INS_MSE:
|
||||
case INS_GET_CHALLENGE:
|
||||
case INS_ACTIVATE_FILE:
|
||||
sw = SW_OK;
|
||||
break;
|
||||
|
||||
default:
|
||||
sw = SW_INS_NOT_SUPPORTED;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
case INS_ACTIVATE_FILE:
|
||||
return;
|
||||
|
||||
default:
|
||||
THROW(SW_INS_NOT_SUPPORTED);
|
||||
break;
|
||||
}
|
||||
THROW(SW_CONDITIONS_NOT_SATISFIED);
|
||||
return sw;
|
||||
}
|
||||
|
||||
void gpg_check_access_read_DO() {
|
||||
unsigned int ref;
|
||||
|
||||
ref = (G_gpg_vstate.io_p1 << 8) | G_gpg_vstate.io_p2;
|
||||
|
||||
switch (ref) {
|
||||
// ALWAYS
|
||||
case 0x0101:
|
||||
case 0x0102:
|
||||
case 0x01F0:
|
||||
case 0x01F1:
|
||||
case 0x01F2:
|
||||
case 0x01F8:
|
||||
case 0x006E:
|
||||
case 0x0065:
|
||||
case 0x0073:
|
||||
case 0x007A:
|
||||
case 0x004F:
|
||||
case 0x005E:
|
||||
case 0x005B:
|
||||
case 0x5F2D:
|
||||
case 0x5F35:
|
||||
case 0x5F50:
|
||||
case 0x5F52:
|
||||
case 0x7F21:
|
||||
case 0x0093:
|
||||
case 0x00C0:
|
||||
case 0x00C1:
|
||||
case 0x00C2:
|
||||
case 0x00C3:
|
||||
case 0x00C4:
|
||||
case 0x00C5:
|
||||
case 0x00C7:
|
||||
case 0x00C8:
|
||||
case 0x00C9:
|
||||
case 0x00C6:
|
||||
case 0x00CA:
|
||||
case 0x00CD:
|
||||
case 0x00CC:
|
||||
case 0x00CE:
|
||||
case 0x00CF:
|
||||
case 0x00D0:
|
||||
case 0x7F74:
|
||||
case 0x7F66:
|
||||
case 0x00D6:
|
||||
case 0x00D7:
|
||||
case 0x00D8:
|
||||
return;
|
||||
|
||||
// PW2
|
||||
case 0x0103:
|
||||
if (gpg_pin_is_verified(PIN_ID_PW2)) {
|
||||
return;
|
||||
}
|
||||
break;
|
||||
|
||||
// PW3
|
||||
case 0x00B6:
|
||||
case 0x00A4:
|
||||
case 0x00B8:
|
||||
case 0x0104:
|
||||
if (gpg_pin_is_verified(PIN_ID_PW3)) {
|
||||
return;
|
||||
/**
|
||||
* Check INS Read access condition
|
||||
* Verify if the corresponding PW is verified
|
||||
*
|
||||
* @return Status Word
|
||||
*
|
||||
*/
|
||||
static int gpg_check_access_read_DO() {
|
||||
int sw = SW_UNKNOWN;
|
||||
|
||||
switch (G_gpg_vstate.io_p1p2) {
|
||||
// ALWAYS
|
||||
case 0x0101:
|
||||
case 0x0102:
|
||||
case 0x01F0:
|
||||
case 0x01F1:
|
||||
case 0x01F2:
|
||||
case 0x01F8:
|
||||
case 0x006E:
|
||||
case 0x0065:
|
||||
case 0x0073:
|
||||
case 0x007A:
|
||||
case 0x004F:
|
||||
case 0x005E:
|
||||
case 0x005B:
|
||||
case 0x5F2D:
|
||||
case 0x5F35:
|
||||
case 0x5F50:
|
||||
case 0x5F52:
|
||||
case 0x7F21:
|
||||
case 0x0093:
|
||||
case 0x00C0:
|
||||
case 0x00C1:
|
||||
case 0x00C2:
|
||||
case 0x00C3:
|
||||
case 0x00C4:
|
||||
case 0x00C5:
|
||||
case 0x00C7:
|
||||
case 0x00C8:
|
||||
case 0x00C9:
|
||||
case 0x00C6:
|
||||
case 0x00CA:
|
||||
case 0x00CD:
|
||||
case 0x00CC:
|
||||
case 0x00CE:
|
||||
case 0x00CF:
|
||||
case 0x00D0:
|
||||
case 0x7F74:
|
||||
case 0x7F66:
|
||||
case 0x00D6:
|
||||
case 0x00D7:
|
||||
case 0x00D8:
|
||||
sw = SW_OK;
|
||||
break;
|
||||
|
||||
// PW2
|
||||
case 0x0103:
|
||||
if (gpg_pin_is_verified(PIN_ID_PW2)) {
|
||||
sw = SW_OK;
|
||||
}
|
||||
break;
|
||||
|
||||
// PW3
|
||||
case 0x00B6:
|
||||
case 0x00A4:
|
||||
case 0x00B8:
|
||||
case 0x0104:
|
||||
if (gpg_pin_is_verified(PIN_ID_PW3)) {
|
||||
sw = SW_OK;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
sw = SW_CONDITIONS_NOT_SATISFIED;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
THROW(SW_CONDITIONS_NOT_SATISFIED);
|
||||
return sw;
|
||||
}
|
||||
|
||||
char debugbuff[5];
|
||||
|
||||
void gpg_check_access_write_DO() {
|
||||
unsigned int ref;
|
||||
gpg_pin_t * pin_pw2, *pin_pw3;
|
||||
|
||||
pin_pw2 = gpg_pin_get_pin(PIN_ID_PW2);
|
||||
pin_pw3 = gpg_pin_get_pin(PIN_ID_PW3);
|
||||
ref = (G_gpg_vstate.io_p1 << 8) | G_gpg_vstate.io_p2;
|
||||
|
||||
switch (ref) {
|
||||
// PW2
|
||||
case 0x0101:
|
||||
case 0x0103:
|
||||
case 0x01F2:
|
||||
if (gpg_pin_is_verified(PIN_ID_PW2)) {
|
||||
return;
|
||||
}
|
||||
break;
|
||||
|
||||
// PW3
|
||||
case 0x3FFF: // only used for putkey under PW3 control
|
||||
case 0x4f:
|
||||
case 0x0102:
|
||||
case 0x0104:
|
||||
case 0x01F1:
|
||||
case 0x01F8:
|
||||
case 0x005E:
|
||||
case 0x005B:
|
||||
case 0x5F2D:
|
||||
case 0x5F35:
|
||||
case 0x5F50:
|
||||
case 0x5F48:
|
||||
case 0x7F21:
|
||||
case 0x00B6:
|
||||
case 0x00A4:
|
||||
case 0x00B8:
|
||||
case 0x00C1:
|
||||
case 0x00C2:
|
||||
case 0x00C3:
|
||||
case 0x00C4:
|
||||
case 0x00C5:
|
||||
case 0x00C7:
|
||||
case 0x00C8:
|
||||
case 0x00C9:
|
||||
case 0x00C6:
|
||||
case 0x00CA:
|
||||
case 0x00CB:
|
||||
case 0x00CC:
|
||||
case 0x00CD:
|
||||
case 0x00CE:
|
||||
case 0x00CF:
|
||||
case 0x00D0:
|
||||
case 0x00D1:
|
||||
case 0x00D2:
|
||||
case 0x00D3:
|
||||
case 0x00D5:
|
||||
case 0x00F4:
|
||||
case 0x00D6:
|
||||
case 0x00D7:
|
||||
case 0x00D8:
|
||||
if (gpg_pin_is_verified(PIN_ID_PW3)) {
|
||||
return;
|
||||
/**
|
||||
* Check INS Write access condition
|
||||
* Verify if the corresponding PW is verified
|
||||
*
|
||||
* @return Status Word
|
||||
*
|
||||
*/
|
||||
static int gpg_check_access_write_DO() {
|
||||
int sw = SW_UNKNOWN;
|
||||
|
||||
switch (G_gpg_vstate.io_p1p2) {
|
||||
// PW2
|
||||
case 0x0101:
|
||||
case 0x0103:
|
||||
case 0x01F2:
|
||||
if (gpg_pin_is_verified(PIN_ID_PW2)) {
|
||||
sw = SW_OK;
|
||||
}
|
||||
break;
|
||||
|
||||
// PW3
|
||||
case 0x3FFF: // only used for putkey under PW3 control
|
||||
case 0x4f:
|
||||
case 0x0102:
|
||||
case 0x0104:
|
||||
case 0x01F1:
|
||||
case 0x01F8:
|
||||
case 0x005E:
|
||||
case 0x005B:
|
||||
case 0x5F2D:
|
||||
case 0x5F35:
|
||||
case 0x5F50:
|
||||
case 0x5F48:
|
||||
case 0x7F21:
|
||||
case 0x00B6:
|
||||
case 0x00A4:
|
||||
case 0x00B8:
|
||||
case 0x00C1:
|
||||
case 0x00C2:
|
||||
case 0x00C3:
|
||||
case 0x00C4:
|
||||
case 0x00C5:
|
||||
case 0x00C7:
|
||||
case 0x00C8:
|
||||
case 0x00C9:
|
||||
case 0x00C6:
|
||||
case 0x00CA:
|
||||
case 0x00CB:
|
||||
case 0x00CC:
|
||||
case 0x00CD:
|
||||
case 0x00CE:
|
||||
case 0x00CF:
|
||||
case 0x00D0:
|
||||
case 0x00D1:
|
||||
case 0x00D2:
|
||||
case 0x00D3:
|
||||
case 0x00D5:
|
||||
case 0x00F4:
|
||||
case 0x00D6:
|
||||
case 0x00D7:
|
||||
case 0x00D8:
|
||||
if (gpg_pin_is_verified(PIN_ID_PW3)) {
|
||||
sw = SW_OK;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
sw = SW_CONDITIONS_NOT_SATISFIED;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
THROW(SW_CONDITIONS_NOT_SATISFIED);
|
||||
return sw;
|
||||
}
|
||||
|
||||
/* assume command is fully received */
|
||||
/**
|
||||
* APDU Handler: dispatch command
|
||||
*
|
||||
* @return Status Word
|
||||
*
|
||||
*/
|
||||
int gpg_dispatch() {
|
||||
unsigned int tag, t, l;
|
||||
int sw;
|
||||
|
||||
if ((G_gpg_vstate.io_cla != 0x00) && (G_gpg_vstate.io_cla != 0x10) && (G_gpg_vstate.io_cla != 0xEF)) {
|
||||
THROW(SW_CLA_NOT_SUPPORTED);
|
||||
return SW_CLA_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
tag = (G_gpg_vstate.io_p1 << 8) | G_gpg_vstate.io_p2;
|
||||
|
||||
switch (G_gpg_vstate.io_ins) {
|
||||
#ifdef GPG_LOG
|
||||
case INS_GET_LOG:
|
||||
gpg_io_discard(1);
|
||||
gpg_io_insert(G_gpg_vstate.log_buffer, 32);
|
||||
return SW_OK;
|
||||
#endif
|
||||
|
||||
/* --- SELECT --- */
|
||||
case INS_SELECT:
|
||||
sw = gpg_apdu_select();
|
||||
return sw;
|
||||
break;
|
||||
unsigned int tag, t, l;
|
||||
int sw = SW_UNKNOWN;
|
||||
|
||||
/* --- ACTIVATE/TERMINATE FILE --- */
|
||||
case INS_ACTIVATE_FILE:
|
||||
gpg_io_discard(0);
|
||||
if (N_gpg_pstate->histo[7] == STATE_TERMINATE) {
|
||||
gpg_install(STATE_ACTIVATE);
|
||||
if ((G_gpg_vstate.io_cla != CLA_APP_DEF) && (G_gpg_vstate.io_cla != CLA_APP_CHAIN) &&
|
||||
(G_gpg_vstate.io_cla != CLA_APP_APDU_PIN)) {
|
||||
return SW_CLA_NOT_SUPPORTED;
|
||||
}
|
||||
return (SW_OK);
|
||||
break;
|
||||
|
||||
case INS_TERMINATE_DF:
|
||||
gpg_io_discard(0);
|
||||
if (gpg_pin_is_verified(PIN_ID_PW3) || (N_gpg_pstate->PW3.counter == 0)) {
|
||||
gpg_install(STATE_TERMINATE);
|
||||
return (SW_OK);
|
||||
break;
|
||||
}
|
||||
THROW(SW_CONDITIONS_NOT_SATISFIED);
|
||||
break;
|
||||
}
|
||||
|
||||
/* Other commands allowed if not terminated */
|
||||
if (N_gpg_pstate->histo[7] != 0x07) {
|
||||
THROW(SW_STATE_TERMINATED);
|
||||
}
|
||||
|
||||
/* Process */
|
||||
gpg_check_access_ins();
|
||||
|
||||
switch (G_gpg_vstate.io_ins) {
|
||||
#ifdef GPG_DEBUG_APDU
|
||||
case 0x42:
|
||||
sw = debug_apdu();
|
||||
break;
|
||||
#endif
|
||||
|
||||
case INS_EXIT:
|
||||
os_sched_exit(0);
|
||||
sw = SW_OK;
|
||||
break;
|
||||
|
||||
/* --- CHALLENGE --- */
|
||||
case INS_GET_CHALLENGE:
|
||||
sw = gpg_apdu_get_challenge();
|
||||
break;
|
||||
|
||||
/* --- DATA --- */
|
||||
switch (G_gpg_vstate.io_ins) {
|
||||
#ifdef GPG_LOG
|
||||
case INS_GET_LOG:
|
||||
gpg_io_discard(1);
|
||||
gpg_io_insert(G_gpg_vstate.log_buffer, sizeof(G_gpg_vstate.log_buffer));
|
||||
return SW_OK;
|
||||
#endif
|
||||
|
||||
case INS_SELECT_DATA:
|
||||
if ((G_gpg_vstate.io_p1 > 2) || (G_gpg_vstate.io_p2 != 0x04)) {
|
||||
THROW(SW_WRONG_P1P2);
|
||||
}
|
||||
gpg_io_fetch_tl(&t, &l);
|
||||
if (t != 0x60) {
|
||||
// TODO add l check
|
||||
THROW(SW_DATA_INVALID);
|
||||
}
|
||||
gpg_io_fetch_tl(&t, &l);
|
||||
if (t != 0x5C) {
|
||||
// TODO add l check
|
||||
THROW(SW_WRONG_DATA);
|
||||
}
|
||||
if (l == 1) {
|
||||
tag = gpg_io_fetch_u8();
|
||||
} else if (l == 2) {
|
||||
tag = gpg_io_fetch_u16();
|
||||
} else {
|
||||
THROW(SW_WRONG_DATA);
|
||||
}
|
||||
sw = gpg_apdu_select_data(tag, G_gpg_vstate.io_p1);
|
||||
break;
|
||||
|
||||
case INS_GET_DATA:
|
||||
gpg_check_access_read_DO();
|
||||
switch (tag) {
|
||||
case 0x00B6:
|
||||
case 0x00A4:
|
||||
case 0x00B8:
|
||||
sw = gpg_apdu_get_key_data(tag);
|
||||
break;
|
||||
default:
|
||||
sw = gpg_apdu_get_data(tag);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
case INS_GET_NEXT_DATA:
|
||||
gpg_check_access_read_DO();
|
||||
sw = gpg_apdu_get_next_data(tag);
|
||||
break;
|
||||
|
||||
case INS_PUT_DATA_ODD:
|
||||
case INS_PUT_DATA:
|
||||
gpg_check_access_write_DO();
|
||||
switch (tag) {
|
||||
case 0x00B6:
|
||||
case 0x00A4:
|
||||
case 0x00B8:
|
||||
sw = gpg_apdu_put_key_data(tag);
|
||||
break;
|
||||
default:
|
||||
sw = gpg_apdu_put_data(tag);
|
||||
break;
|
||||
/* --- SELECT --- */
|
||||
case INS_SELECT:
|
||||
return gpg_apdu_select();
|
||||
|
||||
/* --- ACTIVATE/TERMINATE FILE --- */
|
||||
case INS_ACTIVATE_FILE:
|
||||
gpg_io_discard(0);
|
||||
if (N_gpg_pstate->histo[HISTO_OFFSET_STATE] == STATE_TERMINATE) {
|
||||
gpg_install(STATE_ACTIVATE);
|
||||
}
|
||||
return SW_OK;
|
||||
|
||||
case INS_TERMINATE_DF:
|
||||
gpg_io_discard(0);
|
||||
if (gpg_pin_is_verified(PIN_ID_PW3) || (N_gpg_pstate->PW3.counter == 0)) {
|
||||
gpg_install(STATE_TERMINATE);
|
||||
return SW_OK;
|
||||
}
|
||||
return SW_CONDITIONS_NOT_SATISFIED;
|
||||
}
|
||||
break;
|
||||
|
||||
/* --- PIN -- */
|
||||
case INS_VERIFY:
|
||||
if ((G_gpg_vstate.io_p2 == 0x81) || (G_gpg_vstate.io_p2 == 0x82) || (G_gpg_vstate.io_p2 == 0x83)) {
|
||||
sw = gpg_apdu_verify();
|
||||
break;
|
||||
/* Other commands allowed if not terminated */
|
||||
if (N_gpg_pstate->histo[HISTO_OFFSET_STATE] != STATE_ACTIVATE) {
|
||||
return SW_STATE_TERMINATED;
|
||||
}
|
||||
THROW(SW_INCORRECT_P1P2);
|
||||
|
||||
case INS_CHANGE_REFERENCE_DATA:
|
||||
if ((G_gpg_vstate.io_p2 == 0x81) || (G_gpg_vstate.io_p2 == 0x83)) {
|
||||
sw = gpg_apdu_change_ref_data();
|
||||
break;
|
||||
/* Process */
|
||||
sw = gpg_check_access_ins();
|
||||
if (sw != SW_OK) {
|
||||
return sw;
|
||||
}
|
||||
THROW(SW_INCORRECT_P1P2);
|
||||
|
||||
case INS_RESET_RETRY_COUNTER:
|
||||
if ((G_gpg_vstate.io_p2 == 0x81) && ((G_gpg_vstate.io_p1 == 0) || (G_gpg_vstate.io_p1 == 2))) {
|
||||
sw = gpg_apdu_reset_retry_counter();
|
||||
break;
|
||||
switch (G_gpg_vstate.io_ins) {
|
||||
case INS_EXIT:
|
||||
os_sched_exit(0);
|
||||
sw = SW_OK;
|
||||
break;
|
||||
|
||||
/* --- CHALLENGE --- */
|
||||
case INS_GET_CHALLENGE:
|
||||
sw = gpg_apdu_get_challenge();
|
||||
break;
|
||||
|
||||
/* --- DATA --- */
|
||||
|
||||
case INS_SELECT_DATA:
|
||||
if ((G_gpg_vstate.io_p1 > SELECT_MAX_INSTANCE) || (G_gpg_vstate.io_p2 != SELECT_SKIP)) {
|
||||
sw = SW_WRONG_P1P2;
|
||||
break;
|
||||
}
|
||||
gpg_io_fetch_tl(&t, &l);
|
||||
if (t != 0x60) {
|
||||
sw = SW_WRONG_DATA;
|
||||
break;
|
||||
}
|
||||
gpg_io_fetch_tl(&t, &l);
|
||||
if (t != 0x5C) {
|
||||
sw = SW_WRONG_DATA;
|
||||
break;
|
||||
}
|
||||
if (l == 1) {
|
||||
tag = gpg_io_fetch_u8();
|
||||
} else if (l == 2) {
|
||||
tag = gpg_io_fetch_u16();
|
||||
} else {
|
||||
sw = SW_WRONG_DATA;
|
||||
break;
|
||||
}
|
||||
gpg_apdu_select_data(tag, G_gpg_vstate.io_p1);
|
||||
sw = SW_OK;
|
||||
break;
|
||||
|
||||
case INS_GET_DATA:
|
||||
sw = gpg_check_access_read_DO();
|
||||
if (sw != SW_OK) {
|
||||
break;
|
||||
}
|
||||
switch (G_gpg_vstate.io_p1p2) {
|
||||
case 0x00B6:
|
||||
case 0x00A4:
|
||||
case 0x00B8:
|
||||
sw = gpg_apdu_get_key_data(G_gpg_vstate.io_p1p2);
|
||||
break;
|
||||
default:
|
||||
sw = gpg_apdu_get_data(G_gpg_vstate.io_p1p2);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
case INS_GET_NEXT_DATA:
|
||||
sw = gpg_check_access_read_DO();
|
||||
if (sw != SW_OK) {
|
||||
break;
|
||||
}
|
||||
sw = gpg_apdu_get_next_data(G_gpg_vstate.io_p1p2);
|
||||
break;
|
||||
|
||||
case INS_PUT_DATA_ODD:
|
||||
case INS_PUT_DATA:
|
||||
sw = gpg_check_access_write_DO();
|
||||
if (sw != SW_OK) {
|
||||
break;
|
||||
}
|
||||
switch (G_gpg_vstate.io_p1p2) {
|
||||
case 0x00B6:
|
||||
case 0x00A4:
|
||||
case 0x00B8:
|
||||
sw = gpg_apdu_put_key_data(G_gpg_vstate.io_p1p2);
|
||||
break;
|
||||
default:
|
||||
sw = gpg_apdu_put_data(G_gpg_vstate.io_p1p2);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
/* --- PIN -- */
|
||||
case INS_VERIFY:
|
||||
if ((G_gpg_vstate.io_p2 == PIN_ID_PW1) || (G_gpg_vstate.io_p2 == PIN_ID_PW2) ||
|
||||
(G_gpg_vstate.io_p2 == PIN_ID_PW3)) {
|
||||
sw = gpg_apdu_verify();
|
||||
break;
|
||||
}
|
||||
sw = SW_WRONG_P1P2;
|
||||
break;
|
||||
|
||||
case INS_CHANGE_REFERENCE_DATA:
|
||||
if ((G_gpg_vstate.io_p2 == PIN_ID_PW1) || (G_gpg_vstate.io_p2 == PIN_ID_PW3)) {
|
||||
sw = gpg_apdu_change_ref_data();
|
||||
break;
|
||||
}
|
||||
sw = SW_WRONG_P1P2;
|
||||
break;
|
||||
|
||||
case INS_RESET_RETRY_COUNTER:
|
||||
if ((G_gpg_vstate.io_p2 == PIN_ID_PW1) &&
|
||||
((G_gpg_vstate.io_p1 == RESET_RETRY_WITH_CODE) ||
|
||||
(G_gpg_vstate.io_p1 == RESET_RETRY_WITH_PW3))) {
|
||||
sw = gpg_apdu_reset_retry_counter();
|
||||
break;
|
||||
}
|
||||
sw = SW_WRONG_P1P2;
|
||||
break;
|
||||
|
||||
/* --- Key Management --- */
|
||||
case INS_GEN_ASYM_KEYPAIR:
|
||||
sw = gpg_apdu_gen();
|
||||
break;
|
||||
|
||||
/* --- MSE --- */
|
||||
case INS_MSE:
|
||||
sw = gpg_apdu_mse();
|
||||
break;
|
||||
|
||||
/* --- PSO --- */
|
||||
case INS_PSO:
|
||||
sw = gpg_apdu_pso();
|
||||
break;
|
||||
|
||||
case INS_INTERNAL_AUTHENTICATE:
|
||||
sw = gpg_apdu_internal_authenticate();
|
||||
break;
|
||||
|
||||
default:
|
||||
sw = SW_INS_NOT_SUPPORTED;
|
||||
break;
|
||||
}
|
||||
THROW(SW_INCORRECT_P1P2);
|
||||
|
||||
/* --- Key Management --- */
|
||||
case INS_GEN_ASYM_KEYPAIR:
|
||||
sw = gpg_apdu_gen();
|
||||
break;
|
||||
|
||||
/* --- MSE --- */
|
||||
case INS_MSE:
|
||||
sw = gpg_apdu_mse(tag);
|
||||
break;
|
||||
|
||||
/* --- PSO --- */
|
||||
case INS_PSO:
|
||||
sw = gpg_apdu_pso();
|
||||
break;
|
||||
|
||||
case INS_INTERNAL_AUTHENTICATE:
|
||||
sw = gpg_apdu_internal_authenticate();
|
||||
break;
|
||||
|
||||
default:
|
||||
THROW(SW_INS_NOT_SUPPORTED);
|
||||
break;
|
||||
}
|
||||
|
||||
return sw;
|
||||
return sw;
|
||||
}
|
||||
|
@ -1,284 +1,394 @@
|
||||
/* Copyright 2017 Cedric Mesnil <cslashm@gmail.com>, Ledger SAS
|
||||
/*****************************************************************************
|
||||
* Ledger App OpenPGP.
|
||||
* (c) 2024 Ledger SAS.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*****************************************************************************/
|
||||
|
||||
#include "os.h"
|
||||
#include "cx.h"
|
||||
#include "gpg_types.h"
|
||||
#include "gpg_api.h"
|
||||
#include "gpg_vars.h"
|
||||
#include "cx_ram.h"
|
||||
#include "cx_errors.h"
|
||||
|
||||
/* @in slot slot num [0 ; GPG_KEYS_SLOTS[
|
||||
* @out seed 32 bytes master seed for given slot
|
||||
/**
|
||||
* Derivate the App Path from the Master Seed for a specific slot
|
||||
*
|
||||
* @param[in] slot Selected slot
|
||||
* @param[out] seed 32 bytes seed for given slot
|
||||
*
|
||||
* @return Status Word
|
||||
*
|
||||
*/
|
||||
void gpg_pso_derive_slot_seed(int slot, unsigned char *seed) {
|
||||
unsigned int path[2];
|
||||
unsigned char chain[32];
|
||||
|
||||
os_memset(chain, 0, 32);
|
||||
path[0] = 0x80475047;
|
||||
path[1] = slot + 1;
|
||||
os_perso_derive_node_bip32(CX_CURVE_SECP256K1, path, 2, seed, chain);
|
||||
int gpg_pso_derive_slot_seed(int slot, unsigned char *seed) {
|
||||
unsigned int path[2];
|
||||
unsigned char chain[32];
|
||||
cx_err_t error = CX_INTERNAL_ERROR;
|
||||
|
||||
explicit_bzero(chain, 32);
|
||||
path[0] = 0x80475047;
|
||||
path[1] = slot + 1;
|
||||
CX_CHECK(os_derive_bip32_no_throw(CX_CURVE_SECP256K1, path, 2, seed, chain));
|
||||
|
||||
end:
|
||||
if (error != CX_OK) {
|
||||
return error;
|
||||
}
|
||||
return SW_OK;
|
||||
}
|
||||
|
||||
/* @in Sn master seed slot number
|
||||
* @in key_name key name: 'sig ', 'auth ', 'dec '
|
||||
* @in idx sub-seed index
|
||||
* @out Ski generated sub_seed
|
||||
* @in Ski_len sub-seed length
|
||||
/**
|
||||
* Derivate the Key from the Generated Seed
|
||||
*
|
||||
* @param[in] Sn Seed for the selected slot
|
||||
* @param[in] key_name key name: 'sig ', 'auth ', 'dec '
|
||||
* @param[in] idx sub-seed index
|
||||
* @param[out] Ski generated sub_seed
|
||||
* @param[in] Ski_len sub-seed length
|
||||
*
|
||||
* @return Status Word
|
||||
*
|
||||
*/
|
||||
void gpg_pso_derive_key_seed(unsigned char *Sn,
|
||||
unsigned char *key_name,
|
||||
unsigned int idx,
|
||||
unsigned char *Ski,
|
||||
unsigned int Ski_len) {
|
||||
unsigned char h[32];
|
||||
h[0] = idx >> 8;
|
||||
h[1] = idx;
|
||||
|
||||
cx_sha256_init(&G_gpg_vstate.work.md.sha256);
|
||||
cx_hash((cx_hash_t *)&G_gpg_vstate.work.md.sha256, 0, Sn, 32, NULL, 0);
|
||||
cx_hash((cx_hash_t *)&G_gpg_vstate.work.md.sha256, 0, (unsigned char *)key_name, 4, NULL, 0);
|
||||
cx_hash((cx_hash_t *)&G_gpg_vstate.work.md.sha256, CX_LAST, h, 2, h, 32);
|
||||
#ifdef GPG_SHAKE256
|
||||
cx_shake256_init(&G_gpg_vstate.work.md.sha3, Ski_len);
|
||||
cx_sha3_update(&G_gpg_vstate.work.md.sha3, h, 32);
|
||||
cx_sha3_final(&G_gpg_vstate.work.md.sha3, Ski);
|
||||
#else
|
||||
cx_sha3_xof_init(&G_gpg_vstate.work.md.sha3, 256, Ski_len);
|
||||
cx_hash((cx_hash_t *)&G_gpg_vstate.work.md.sha3, CX_LAST, h, 32, Ski, Ski_len);
|
||||
#endif
|
||||
int gpg_pso_derive_key_seed(unsigned char *Sn,
|
||||
unsigned char *key_name,
|
||||
unsigned int idx,
|
||||
unsigned char *Ski,
|
||||
unsigned int Ski_len) {
|
||||
unsigned char h[32];
|
||||
cx_err_t error = CX_INTERNAL_ERROR;
|
||||
U2BE_ENCODE(h, 0, idx);
|
||||
|
||||
cx_sha256_init(&G_gpg_vstate.work.md.sha256);
|
||||
CX_CHECK(cx_hash_no_throw((cx_hash_t *) &G_gpg_vstate.work.md.sha256, 0, Sn, 32, NULL, 0));
|
||||
CX_CHECK(cx_hash_no_throw((cx_hash_t *) &G_gpg_vstate.work.md.sha256,
|
||||
0,
|
||||
(unsigned char *) key_name,
|
||||
4,
|
||||
NULL,
|
||||
0));
|
||||
CX_CHECK(cx_hash_no_throw((cx_hash_t *) &G_gpg_vstate.work.md.sha256, CX_LAST, h, 2, h, 32));
|
||||
CX_CHECK(cx_shake256_init_no_throw(&G_gpg_vstate.work.md.sha3, Ski_len));
|
||||
CX_CHECK(cx_sha3_update(&G_gpg_vstate.work.md.sha3, h, 32));
|
||||
CX_CHECK(cx_sha3_final(&G_gpg_vstate.work.md.sha3, Ski));
|
||||
|
||||
end:
|
||||
if (error != CX_OK) {
|
||||
return error;
|
||||
}
|
||||
return SW_OK;
|
||||
}
|
||||
|
||||
/* assume command is fully received */
|
||||
int gpg_apdu_gen() {
|
||||
unsigned int t, l, ksz, reset_cnt;
|
||||
gpg_key_t * keygpg;
|
||||
unsigned char seed[66];
|
||||
unsigned char *name;
|
||||
|
||||
switch ((G_gpg_vstate.io_p1 << 8) | G_gpg_vstate.io_p2) {
|
||||
case 0x8000:
|
||||
case 0x8001:
|
||||
case 0x8100:
|
||||
break;
|
||||
default:
|
||||
THROW(SW_INCORRECT_P1P2);
|
||||
return SW_INCORRECT_P1P2;
|
||||
}
|
||||
|
||||
if (G_gpg_vstate.io_lc != 2) {
|
||||
THROW(SW_WRONG_LENGTH);
|
||||
return SW_WRONG_LENGTH;
|
||||
}
|
||||
|
||||
gpg_io_fetch_tl(&t, &l);
|
||||
gpg_io_discard(1);
|
||||
reset_cnt = 0;
|
||||
switch (t) {
|
||||
case 0xB6:
|
||||
keygpg = &G_gpg_vstate.kslot->sig;
|
||||
name = (unsigned char *)PIC("sig ");
|
||||
reset_cnt = 0;
|
||||
break;
|
||||
case 0xA4:
|
||||
keygpg = &G_gpg_vstate.kslot->aut;
|
||||
name = (unsigned char *)PIC("aut ");
|
||||
break;
|
||||
case 0xB8:
|
||||
keygpg = &G_gpg_vstate.kslot->dec;
|
||||
name = (unsigned char *)PIC("dec ");
|
||||
break;
|
||||
default:
|
||||
THROW(SW_WRONG_DATA);
|
||||
return SW_WRONG_DATA;
|
||||
}
|
||||
|
||||
switch ((G_gpg_vstate.io_p1 << 8) | G_gpg_vstate.io_p2) {
|
||||
// -- generate keypair ---
|
||||
case 0x8000:
|
||||
case 0x8001:
|
||||
// RSA
|
||||
if (keygpg->attributes.value[0] == 0x01) {
|
||||
unsigned char * pq;
|
||||
cx_rsa_public_key_t * rsa_pub;
|
||||
cx_rsa_private_key_t *rsa_priv, *pkey;
|
||||
unsigned int pkey_size;
|
||||
|
||||
ksz = (keygpg->attributes.value[1] << 8) | keygpg->attributes.value[2];
|
||||
ksz = ksz >> 3;
|
||||
rsa_pub = (cx_rsa_public_key_t *)&G_gpg_vstate.work.rsa.public;
|
||||
rsa_priv = (cx_rsa_private_key_t *)&G_gpg_vstate.work.rsa.private;
|
||||
pkey = &keygpg->priv_key.rsa;
|
||||
switch (ksz) {
|
||||
case 1024 / 8:
|
||||
pkey_size = sizeof(cx_rsa_1024_private_key_t);
|
||||
break;
|
||||
case 2048 / 8:
|
||||
pkey_size = sizeof(cx_rsa_2048_private_key_t);
|
||||
break;
|
||||
case 3072 / 8:
|
||||
pkey_size = sizeof(cx_rsa_3072_private_key_t);
|
||||
break;
|
||||
case 4096 / 8:
|
||||
pkey_size = sizeof(cx_rsa_4096_private_key_t);
|
||||
break;
|
||||
default:
|
||||
THROW(SW_WRONG_DATA);
|
||||
}
|
||||
pq = NULL;
|
||||
if ((G_gpg_vstate.io_p2 == 0x01) || (G_gpg_vstate.seed_mode)) {
|
||||
/**
|
||||
* Generate a RSA key pair and writes it in NVRam
|
||||
*
|
||||
* @param[in] keygpg pointer on key structure
|
||||
* @param[in] name key name: 'sig ', 'auth ', 'dec '
|
||||
*
|
||||
* @return Status Word
|
||||
*
|
||||
*/
|
||||
static int gpg_gen_rsa_kyey(gpg_key_t *keygpg, uint8_t *name) {
|
||||
cx_rsa_public_key_t *rsa_pub = NULL;
|
||||
cx_rsa_private_key_t *rsa_priv = NULL;
|
||||
uint8_t *pq = NULL;
|
||||
uint32_t ksz = 0, reset_cnt = 0, pkey_size = 0;
|
||||
int sw = SW_UNKNOWN;
|
||||
cx_err_t error = CX_INTERNAL_ERROR;
|
||||
uint8_t seed[66] = {0};
|
||||
|
||||
ksz = U2BE(keygpg->attributes.value, 1) >> 3;
|
||||
rsa_pub = (cx_rsa_public_key_t *) &G_gpg_vstate.work.rsa.public;
|
||||
rsa_priv = (cx_rsa_private_key_t *) &G_gpg_vstate.work.rsa.private;
|
||||
switch (ksz) {
|
||||
case 2048 / 8:
|
||||
pkey_size = sizeof(cx_rsa_2048_private_key_t);
|
||||
break;
|
||||
case 3072 / 8:
|
||||
pkey_size = sizeof(cx_rsa_3072_private_key_t);
|
||||
break;
|
||||
#ifdef WITH_SUPPORT_RSA4096
|
||||
case 4096 / 8:
|
||||
pkey_size = sizeof(cx_rsa_4096_private_key_t);
|
||||
break;
|
||||
#endif
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (pkey_size == 0) {
|
||||
return SW_WRONG_DATA;
|
||||
}
|
||||
|
||||
if ((G_gpg_vstate.io_p2 == SEEDED_MODE) || (G_gpg_vstate.seed_mode)) {
|
||||
pq = &rsa_pub->n[0];
|
||||
unsigned int size;
|
||||
size = ksz >> 1;
|
||||
gpg_pso_derive_slot_seed(G_gpg_vstate.slot, seed);
|
||||
gpg_pso_derive_key_seed(seed, name, 1, pq, size);
|
||||
gpg_pso_derive_key_seed(seed, name, 2, pq + size, size);
|
||||
sw = gpg_pso_derive_slot_seed(G_gpg_vstate.slot, seed);
|
||||
if (sw != SW_OK) {
|
||||
return sw;
|
||||
}
|
||||
sw = gpg_pso_derive_key_seed(seed, name, 1, pq, size);
|
||||
if (sw != SW_OK) {
|
||||
return sw;
|
||||
}
|
||||
sw = gpg_pso_derive_key_seed(seed, name, 2, pq + size, size);
|
||||
if (sw != SW_OK) {
|
||||
return sw;
|
||||
}
|
||||
*pq |= 0x80;
|
||||
*(pq + size) |= 0x80;
|
||||
cx_math_next_prime(pq, size);
|
||||
cx_math_next_prime(pq + size, size);
|
||||
}
|
||||
CX_CHECK(cx_math_next_prime_no_throw(pq, size));
|
||||
CX_CHECK(cx_math_next_prime_no_throw(pq + size, size));
|
||||
}
|
||||
|
||||
CX_CHECK(
|
||||
cx_rsa_generate_pair_no_throw(ksz,
|
||||
rsa_pub,
|
||||
rsa_priv,
|
||||
(const unsigned char *) N_gpg_pstate->default_RSA_exponent,
|
||||
4,
|
||||
pq));
|
||||
|
||||
nvm_write(&keygpg->priv_key.rsa, rsa_priv, pkey_size);
|
||||
nvm_write(&keygpg->pub_key.rsa[0], rsa_pub->e, 4);
|
||||
|
||||
cx_rsa_generate_pair(ksz, rsa_pub, rsa_priv, N_gpg_pstate->default_RSA_exponent, 4, pq);
|
||||
nvm_write(&G_gpg_vstate.kslot->sig_count, &reset_cnt, sizeof(unsigned int));
|
||||
gpg_io_clear();
|
||||
return SW_OK;
|
||||
|
||||
nvm_write(pkey, rsa_priv, pkey_size);
|
||||
nvm_write(&keygpg->pub_key.rsa[0], rsa_pub->e, 4);
|
||||
if (reset_cnt) {
|
||||
reset_cnt = 0;
|
||||
nvm_write(&G_gpg_vstate.kslot->sig_count, &reset_cnt, sizeof(unsigned int));
|
||||
}
|
||||
gpg_io_clear();
|
||||
end:
|
||||
return error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a RSA key pair
|
||||
*
|
||||
* @param[in] keygpg pointer on key structure
|
||||
*
|
||||
* @return Status Word
|
||||
*
|
||||
*/
|
||||
static int gpg_read_rsa_kyey(gpg_key_t *keygpg) {
|
||||
uint32_t ksz = 0;
|
||||
|
||||
goto send_rsa_pub;
|
||||
gpg_io_discard(1);
|
||||
// check length
|
||||
ksz = U2BE(keygpg->attributes.value, 1) >> 3;
|
||||
gpg_io_mark();
|
||||
switch (ksz) {
|
||||
case 2048 / 8:
|
||||
if (keygpg->priv_key.rsa2048.size == 0) {
|
||||
return SW_REFERENCED_DATA_NOT_FOUND;
|
||||
}
|
||||
gpg_io_insert_tlv(0x81, ksz, (unsigned char *) &keygpg->priv_key.rsa2048.n);
|
||||
break;
|
||||
case 3072 / 8:
|
||||
if (keygpg->priv_key.rsa3072.size == 0) {
|
||||
return SW_REFERENCED_DATA_NOT_FOUND;
|
||||
}
|
||||
gpg_io_insert_tlv(0x81, ksz, (unsigned char *) &keygpg->priv_key.rsa3072.n);
|
||||
break;
|
||||
#ifdef WITH_SUPPORT_RSA4096
|
||||
case 4096 / 8:
|
||||
if (keygpg->priv_key.rsa4096.size == 0) {
|
||||
return SW_REFERENCED_DATA_NOT_FOUND;
|
||||
}
|
||||
gpg_io_insert_tlv(0x81, ksz, (unsigned char *) &keygpg->priv_key.rsa4096.n);
|
||||
break;
|
||||
#endif
|
||||
default:
|
||||
return SW_REFERENCED_DATA_NOT_FOUND;
|
||||
}
|
||||
// ECC
|
||||
if ((keygpg->attributes.value[0] == 18) || (keygpg->attributes.value[0] == 19) ||
|
||||
(keygpg->attributes.value[0] == 22)) {
|
||||
unsigned int curve, keepprivate;
|
||||
keepprivate = 0;
|
||||
curve = gpg_oid2curve(keygpg->attributes.value + 1, keygpg->attributes.length - 1);
|
||||
if (curve == CX_CURVE_NONE) {
|
||||
THROW(SW_REFERENCED_DATA_NOT_FOUND);
|
||||
gpg_io_insert_tlv(0x82, 4, keygpg->pub_key.rsa);
|
||||
|
||||
return SW_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate an Elliptic Curve key pair and writes it in NVRam
|
||||
*
|
||||
* @param[in] keygpg pointer on key structure
|
||||
* @param[in] name key name: 'sig ', 'auth ', 'dec '
|
||||
*
|
||||
* @return Status Word
|
||||
*
|
||||
*/
|
||||
static int gpg_gen_ecc_kyey(gpg_key_t *keygpg, uint8_t *name) {
|
||||
uint32_t curve = 0, keepprivate = 0;
|
||||
uint32_t ksz = 0, reset_cnt = 0;
|
||||
int sw = SW_UNKNOWN;
|
||||
cx_err_t error = CX_INTERNAL_ERROR;
|
||||
uint8_t seed[66] = {0};
|
||||
|
||||
curve = gpg_oid2curve(keygpg->attributes.value + 1, keygpg->attributes.length - 1);
|
||||
if (curve == CX_CURVE_NONE) {
|
||||
return SW_REFERENCED_DATA_NOT_FOUND;
|
||||
}
|
||||
if ((G_gpg_vstate.io_p2 == 0x01) || (G_gpg_vstate.seed_mode)) {
|
||||
ksz = gpg_curve2domainlen(curve);
|
||||
gpg_pso_derive_slot_seed(G_gpg_vstate.slot, seed);
|
||||
gpg_pso_derive_key_seed(seed, name, 1, seed, ksz);
|
||||
cx_ecfp_init_private_key(curve, seed, ksz, &G_gpg_vstate.work.ecfp.private);
|
||||
keepprivate = 1;
|
||||
}
|
||||
|
||||
cx_ecfp_generate_pair(curve, &G_gpg_vstate.work.ecfp.public, &G_gpg_vstate.work.ecfp.private, keepprivate);
|
||||
nvm_write(&keygpg->priv_key.ecfp, &G_gpg_vstate.work.ecfp.private, sizeof(cx_ecfp_private_key_t));
|
||||
nvm_write(&keygpg->pub_key.ecfp, &G_gpg_vstate.work.ecfp.public, sizeof(cx_ecfp_public_key_t));
|
||||
if (reset_cnt) {
|
||||
reset_cnt = 0;
|
||||
nvm_write(&G_gpg_vstate.kslot->sig_count, &reset_cnt, sizeof(unsigned int));
|
||||
}
|
||||
gpg_io_clear();
|
||||
goto send_ecc_pub;
|
||||
}
|
||||
break;
|
||||
|
||||
// --- read pubkey ---
|
||||
case 0x8100:
|
||||
if (keygpg->attributes.value[0] == 0x01) {
|
||||
/// read RSA
|
||||
send_rsa_pub:
|
||||
gpg_io_discard(1);
|
||||
// check length
|
||||
ksz = (keygpg->attributes.value[1] << 8) | keygpg->attributes.value[2];
|
||||
ksz = ksz >> 3;
|
||||
gpg_io_mark();
|
||||
switch (ksz) {
|
||||
case 1024 / 8:
|
||||
if (keygpg->priv_key.rsa1024.size == 0) {
|
||||
THROW(SW_REFERENCED_DATA_NOT_FOUND);
|
||||
return SW_REFERENCED_DATA_NOT_FOUND;
|
||||
}
|
||||
gpg_io_insert_tlv(0x81, ksz, (unsigned char *)&keygpg->priv_key.rsa1024.n);
|
||||
break;
|
||||
case 2048 / 8:
|
||||
if (keygpg->priv_key.rsa2048.size == 0) {
|
||||
THROW(SW_REFERENCED_DATA_NOT_FOUND);
|
||||
return SW_REFERENCED_DATA_NOT_FOUND;
|
||||
}
|
||||
gpg_io_insert_tlv(0x81, ksz, (unsigned char *)&keygpg->priv_key.rsa2048.n);
|
||||
break;
|
||||
case 3072 / 8:
|
||||
if (keygpg->priv_key.rsa3072.size == 0) {
|
||||
THROW(SW_REFERENCED_DATA_NOT_FOUND);
|
||||
return SW_REFERENCED_DATA_NOT_FOUND;
|
||||
if ((G_gpg_vstate.io_p2 == SEEDED_MODE) || (G_gpg_vstate.seed_mode)) {
|
||||
ksz = gpg_curve2domainlen(curve);
|
||||
sw = gpg_pso_derive_slot_seed(G_gpg_vstate.slot, seed);
|
||||
if (sw != SW_OK) {
|
||||
return sw;
|
||||
}
|
||||
gpg_io_insert_tlv(0x81, ksz, (unsigned char *)&keygpg->priv_key.rsa3072.n);
|
||||
break;
|
||||
case 4096 / 8:
|
||||
if (keygpg->priv_key.rsa4096.size == 0) {
|
||||
THROW(SW_REFERENCED_DATA_NOT_FOUND);
|
||||
return SW_REFERENCED_DATA_NOT_FOUND;
|
||||
sw = gpg_pso_derive_key_seed(seed, name, 1, seed, ksz);
|
||||
if (sw != SW_OK) {
|
||||
return sw;
|
||||
}
|
||||
gpg_io_insert_tlv(0x81, ksz, (unsigned char *)&keygpg->priv_key.rsa4096.n);
|
||||
break;
|
||||
}
|
||||
gpg_io_insert_tlv(0x82, 4, keygpg->pub_key.rsa);
|
||||
CX_CHECK(
|
||||
cx_ecfp_init_private_key_no_throw(curve, seed, ksz, &G_gpg_vstate.work.ecfp.private));
|
||||
keepprivate = 1;
|
||||
}
|
||||
|
||||
l = G_gpg_vstate.io_length;
|
||||
gpg_io_set_offset(IO_OFFSET_MARK);
|
||||
gpg_io_insert_tl(0x7f49, l);
|
||||
gpg_io_set_offset(IO_OFFSET_END);
|
||||
CX_CHECK(cx_ecfp_generate_pair_no_throw(curve,
|
||||
&G_gpg_vstate.work.ecfp.public,
|
||||
&G_gpg_vstate.work.ecfp.private,
|
||||
keepprivate));
|
||||
nvm_write(&keygpg->priv_key.ecfp,
|
||||
&G_gpg_vstate.work.ecfp.private,
|
||||
sizeof(cx_ecfp_private_key_t));
|
||||
nvm_write(&keygpg->pub_key.ecfp, &G_gpg_vstate.work.ecfp.public, sizeof(cx_ecfp_public_key_t));
|
||||
|
||||
return SW_OK;
|
||||
}
|
||||
nvm_write(&G_gpg_vstate.kslot->sig_count, &reset_cnt, sizeof(unsigned int));
|
||||
gpg_io_clear();
|
||||
error = SW_OK;
|
||||
|
||||
end:
|
||||
return error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read an Elliptic Curve key pair
|
||||
*
|
||||
* @param[in] keygpg pointer on key structure
|
||||
*
|
||||
* @return Status Word
|
||||
*
|
||||
*/
|
||||
static int gpg_read_ecc_kyey(gpg_key_t *keygpg) {
|
||||
uint32_t curve = 0;
|
||||
uint32_t i, len;
|
||||
cx_err_t error = CX_INTERNAL_ERROR;
|
||||
|
||||
if ((keygpg->attributes.value[0] == 18) || (keygpg->attributes.value[0] == 19) ||
|
||||
(keygpg->attributes.value[0] == 22)) {
|
||||
unsigned int curve;
|
||||
/// read ECC
|
||||
send_ecc_pub:
|
||||
|
||||
if (keygpg->pub_key.ecfp256.W_len == 0) {
|
||||
THROW(SW_REFERENCED_DATA_NOT_FOUND);
|
||||
return 0;
|
||||
}
|
||||
gpg_io_discard(1);
|
||||
gpg_io_mark();
|
||||
curve = gpg_oid2curve(keygpg->attributes.value + 1, keygpg->attributes.length - 1);
|
||||
if (curve == CX_CURVE_Ed25519) {
|
||||
os_memmove(G_gpg_vstate.work.io_buffer + 128, keygpg->pub_key.ecfp256.W, keygpg->pub_key.ecfp256.W_len);
|
||||
cx_edward_compress_point(CX_CURVE_Ed25519, G_gpg_vstate.work.io_buffer + 128, 65);
|
||||
gpg_io_insert_tlv(0x86, 32, G_gpg_vstate.work.io_buffer + 129); // 129: discard 02
|
||||
} else if (curve == CX_CURVE_Curve25519) {
|
||||
unsigned int i, len;
|
||||
len = keygpg->pub_key.ecfp256.W_len - 1;
|
||||
if (keygpg->pub_key.ecfp.W_len == 0) {
|
||||
return SW_REFERENCED_DATA_NOT_FOUND;
|
||||
}
|
||||
gpg_io_discard(1);
|
||||
gpg_io_mark();
|
||||
curve = gpg_oid2curve(keygpg->attributes.value + 1, keygpg->attributes.length - 1);
|
||||
if (curve == CX_CURVE_Ed25519) {
|
||||
memmove(G_gpg_vstate.work.io_buffer + 128,
|
||||
keygpg->pub_key.ecfp.W,
|
||||
keygpg->pub_key.ecfp.W_len);
|
||||
CX_CHECK(cx_edwards_compress_point_no_throw(CX_CURVE_Ed25519,
|
||||
G_gpg_vstate.work.io_buffer + 128,
|
||||
65));
|
||||
gpg_io_insert_tlv(0x86, 32,
|
||||
G_gpg_vstate.work.io_buffer + 129); // 129: discard 02
|
||||
} else if (curve == CX_CURVE_Curve25519) {
|
||||
len = keygpg->pub_key.ecfp.W_len - 1;
|
||||
for (i = 0; i <= len; i++) {
|
||||
G_gpg_vstate.work.io_buffer[128 + i] = keygpg->pub_key.ecfp256.W[len - i];
|
||||
G_gpg_vstate.work.io_buffer[128 + i] = keygpg->pub_key.ecfp.W[len - i];
|
||||
}
|
||||
gpg_io_insert_tlv(0x86, 32, G_gpg_vstate.work.io_buffer + 128);
|
||||
} else {
|
||||
gpg_io_insert_tlv(0x86, keygpg->pub_key.ecfp256.W_len, (unsigned char *)&keygpg->pub_key.ecfp256.W);
|
||||
}
|
||||
l = G_gpg_vstate.io_length;
|
||||
gpg_io_set_offset(IO_OFFSET_MARK);
|
||||
gpg_io_insert_tl(0x7f49, l);
|
||||
gpg_io_set_offset(IO_OFFSET_END);
|
||||
return SW_OK;
|
||||
} else {
|
||||
gpg_io_insert_tlv(0x86,
|
||||
keygpg->pub_key.ecfp.W_len,
|
||||
(unsigned char *) &keygpg->pub_key.ecfp.W);
|
||||
}
|
||||
break;
|
||||
}
|
||||
error = SW_OK;
|
||||
|
||||
end:
|
||||
return error;
|
||||
}
|
||||
|
||||
/**
|
||||
* APDU handler to Generate/Read key pair
|
||||
*
|
||||
* @return Status Word
|
||||
*
|
||||
*/
|
||||
int gpg_apdu_gen() {
|
||||
uint32_t t, l;
|
||||
gpg_key_t *keygpg = NULL;
|
||||
uint8_t *name = NULL;
|
||||
int sw = SW_UNKNOWN;
|
||||
|
||||
THROW(SW_WRONG_DATA);
|
||||
return SW_WRONG_DATA;
|
||||
switch (G_gpg_vstate.io_p1p2) {
|
||||
case GEN_ASYM_KEY:
|
||||
case GEN_ASYM_KEY_SEED:
|
||||
case READ_ASYM_KEY:
|
||||
break;
|
||||
default:
|
||||
return SW_WRONG_P1P2;
|
||||
}
|
||||
|
||||
if (G_gpg_vstate.io_lc != 2) {
|
||||
return SW_WRONG_LENGTH;
|
||||
}
|
||||
|
||||
gpg_io_fetch_tl(&t, &l);
|
||||
gpg_io_discard(1);
|
||||
switch (t) {
|
||||
case KEY_SIG:
|
||||
keygpg = &G_gpg_vstate.kslot->sig;
|
||||
name = (unsigned char *) PIC("sig ");
|
||||
break;
|
||||
case KEY_AUT:
|
||||
keygpg = &G_gpg_vstate.kslot->aut;
|
||||
name = (unsigned char *) PIC("aut ");
|
||||
break;
|
||||
case KEY_DEC:
|
||||
keygpg = &G_gpg_vstate.kslot->dec;
|
||||
name = (unsigned char *) PIC("dec ");
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (keygpg == NULL) {
|
||||
return SW_WRONG_DATA;
|
||||
}
|
||||
|
||||
switch (G_gpg_vstate.io_p1p2) {
|
||||
// -- generate keypair ---
|
||||
case GEN_ASYM_KEY:
|
||||
case GEN_ASYM_KEY_SEED:
|
||||
|
||||
if (keygpg->attributes.value[0] == KEY_ID_RSA) {
|
||||
sw = gpg_gen_rsa_kyey(keygpg, name);
|
||||
if (sw != SW_OK) {
|
||||
break;
|
||||
}
|
||||
} else if ((keygpg->attributes.value[0] == KEY_ID_ECDH) ||
|
||||
(keygpg->attributes.value[0] == KEY_ID_ECDSA) ||
|
||||
(keygpg->attributes.value[0] == KEY_ID_EDDSA)) {
|
||||
sw = gpg_gen_ecc_kyey(keygpg, name);
|
||||
if (sw != SW_OK) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
__attribute__((fallthrough));
|
||||
// --- read pubkey ---
|
||||
case READ_ASYM_KEY:
|
||||
if (keygpg->attributes.value[0] == KEY_ID_RSA) {
|
||||
sw = gpg_read_rsa_kyey(keygpg);
|
||||
} else if ((keygpg->attributes.value[0] == KEY_ID_ECDH) ||
|
||||
(keygpg->attributes.value[0] == KEY_ID_ECDSA) ||
|
||||
(keygpg->attributes.value[0] == KEY_ID_EDDSA)) {
|
||||
sw = gpg_read_ecc_kyey(keygpg);
|
||||
}
|
||||
l = G_gpg_vstate.io_length;
|
||||
gpg_io_set_offset(IO_OFFSET_MARK);
|
||||
gpg_io_insert_tl(0x7f49, l);
|
||||
gpg_io_set_offset(IO_OFFSET_END);
|
||||
break;
|
||||
}
|
||||
return sw;
|
||||
}
|
||||
|
@ -1,72 +1,83 @@
|
||||
/* Copyright 2017 Cedric Mesnil <cslashm@gmail.com>, Ledger SAS
|
||||
/*****************************************************************************
|
||||
* Ledger App OpenPGP.
|
||||
* (c) 2024 Ledger SAS.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*****************************************************************************/
|
||||
|
||||
#include "os.h"
|
||||
#include "cx.h"
|
||||
#include "gpg_types.h"
|
||||
#include "gpg_api.h"
|
||||
#include "gpg_vars.h"
|
||||
|
||||
static int gpg_mse_set(int crt, int ref) {
|
||||
if (crt == 0xA4) {
|
||||
if (ref == 0x02) {
|
||||
G_gpg_vstate.mse_aut = &G_gpg_vstate.kslot->dec;
|
||||
}
|
||||
if (ref == 0x03) {
|
||||
G_gpg_vstate.mse_aut = &G_gpg_vstate.kslot->aut;
|
||||
/**
|
||||
* Set a new MSE configuration
|
||||
*
|
||||
* @param[in] crt selected key
|
||||
* @param[in] ref new operation type
|
||||
*
|
||||
*/
|
||||
static void gpg_mse_set(int crt, int ref) {
|
||||
if (crt == KEY_AUT) {
|
||||
if (ref == 0x02) {
|
||||
G_gpg_vstate.mse_aut = &G_gpg_vstate.kslot->dec;
|
||||
}
|
||||
if (ref == 0x03) {
|
||||
G_gpg_vstate.mse_aut = &G_gpg_vstate.kslot->aut;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (crt == 0xB8) {
|
||||
if (ref == 0x02) {
|
||||
G_gpg_vstate.mse_dec = &G_gpg_vstate.kslot->dec;
|
||||
}
|
||||
if (ref == 0x03) {
|
||||
G_gpg_vstate.mse_dec = &G_gpg_vstate.kslot->aut;
|
||||
if (crt == KEY_DEC) {
|
||||
if (ref == 0x02) {
|
||||
G_gpg_vstate.mse_dec = &G_gpg_vstate.kslot->dec;
|
||||
}
|
||||
if (ref == 0x03) {
|
||||
G_gpg_vstate.mse_dec = &G_gpg_vstate.kslot->aut;
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int gpg_mse_reset() {
|
||||
gpg_mse_set(0xA4, 0x03);
|
||||
gpg_mse_set(0xB8, 0x02);
|
||||
return 0;
|
||||
/**
|
||||
* Reset MSE config
|
||||
*
|
||||
*/
|
||||
void gpg_mse_reset() {
|
||||
gpg_mse_set(KEY_AUT, 0x03);
|
||||
gpg_mse_set(KEY_DEC, 0x02);
|
||||
}
|
||||
|
||||
/**
|
||||
* APDU handler to Manage Security Environment
|
||||
*
|
||||
* @return Status Word
|
||||
*
|
||||
*/
|
||||
int gpg_apdu_mse() {
|
||||
int crt, ref;
|
||||
int crt, ref;
|
||||
|
||||
if ((G_gpg_vstate.io_p1 != 0x41) || ((G_gpg_vstate.io_p2 != 0xA4) && (G_gpg_vstate.io_p2 != 0xB8))) {
|
||||
THROW(SW_INCORRECT_P1P2);
|
||||
return SW_INCORRECT_P1P2;
|
||||
}
|
||||
if ((G_gpg_vstate.io_p1 != MSE_SET) ||
|
||||
((G_gpg_vstate.io_p2 != KEY_AUT) && (G_gpg_vstate.io_p2 != KEY_DEC))) {
|
||||
return SW_WRONG_P1P2;
|
||||
}
|
||||
|
||||
crt = gpg_io_fetch_u16();
|
||||
if (crt != 0x8301) {
|
||||
THROW(SW_WRONG_DATA);
|
||||
return SW_WRONG_DATA;
|
||||
}
|
||||
crt = gpg_io_fetch_u16();
|
||||
if (crt != 0x8301) {
|
||||
return SW_WRONG_DATA;
|
||||
}
|
||||
|
||||
ref = gpg_io_fetch_u8();
|
||||
if ((ref != 0x02) && (ref != 0x03)) {
|
||||
THROW(SW_WRONG_DATA);
|
||||
return SW_WRONG_DATA;
|
||||
}
|
||||
ref = gpg_io_fetch_u8();
|
||||
if ((ref != 0x02) && (ref != 0x03)) {
|
||||
return SW_WRONG_DATA;
|
||||
}
|
||||
|
||||
gpg_mse_set(crt, ref);
|
||||
gpg_io_discard(1);
|
||||
return SW_OK;
|
||||
}
|
||||
gpg_mse_set(G_gpg_vstate.io_p2, ref);
|
||||
gpg_io_discard(1);
|
||||
return SW_OK;
|
||||
}
|
||||
|
@ -1,26 +0,0 @@
|
||||
/* Copyright 2017 Cedric Mesnil <cslashm@gmail.com>, Ledger SAS
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include "os.h"
|
||||
#include "cx.h"
|
||||
#include "gpg_types.h"
|
||||
#include "gpg_api.h"
|
||||
#include "gpg_vars.h"
|
||||
|
||||
#if defined(TARGET_NANOX) || defined(TARGET_NANOS2)
|
||||
const gpg_nv_state_t N_state_pic;
|
||||
#else
|
||||
gpg_nv_state_t N_state_pic;
|
||||
#endif
|
@ -1,280 +1,365 @@
|
||||
/* Copyright 2017 Cedric Mesnil <cslashm@gmail.com>, Ledger SAS
|
||||
/*****************************************************************************
|
||||
* Ledger App OpenPGP.
|
||||
* (c) 2024 Ledger SAS.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*****************************************************************************/
|
||||
|
||||
#include "os.h"
|
||||
#include "cx.h"
|
||||
#include "gpg_types.h"
|
||||
#include "gpg_api.h"
|
||||
#include "gpg_vars.h"
|
||||
#include "gpg_ux.h"
|
||||
|
||||
#include "gpg_ux_nanos.h"
|
||||
|
||||
/**
|
||||
* Get Pin structure from reference ID value
|
||||
*
|
||||
* @param[in] pinref PinCode reference ID
|
||||
*
|
||||
* @return Pin structure, or NULL if invalid
|
||||
*
|
||||
*/
|
||||
gpg_pin_t *gpg_pin_get_pin(int pinref) {
|
||||
switch (pinref) {
|
||||
case PIN_ID_PW1:
|
||||
case PIN_ID_PW2:
|
||||
return &N_gpg_pstate->PW1;
|
||||
case PIN_ID_PW3:
|
||||
return &N_gpg_pstate->PW3;
|
||||
case PIN_ID_RC:
|
||||
return &N_gpg_pstate->RC;
|
||||
}
|
||||
return NULL;
|
||||
switch (pinref) {
|
||||
case PIN_ID_PW1:
|
||||
case PIN_ID_PW2:
|
||||
return (gpg_pin_t *) &N_gpg_pstate->PW1;
|
||||
case PIN_ID_PW3:
|
||||
return (gpg_pin_t *) &N_gpg_pstate->PW3;
|
||||
case PIN_ID_RC:
|
||||
return (gpg_pin_t *) &N_gpg_pstate->RC;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Pin index from reference ID value
|
||||
*
|
||||
* @param[in] pinref PinCode reference ID
|
||||
*
|
||||
* @return Pin index
|
||||
*
|
||||
*/
|
||||
static int gpg_pin_get_state_index(unsigned int pinref) {
|
||||
switch (pinref) {
|
||||
case PIN_ID_PW1:
|
||||
return 1;
|
||||
case PIN_ID_PW2:
|
||||
return 2;
|
||||
case PIN_ID_PW3:
|
||||
return 3;
|
||||
case PIN_ID_RC:
|
||||
return 4;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int gpg_pin_check_internal(gpg_pin_t *pin, unsigned char *pin_val, int pin_len) {
|
||||
unsigned int counter;
|
||||
|
||||
if (pin->counter == 0) {
|
||||
return SW_PIN_BLOCKED;
|
||||
}
|
||||
|
||||
counter = pin->counter - 1;
|
||||
gpg_nvm_write(&(pin->counter), &counter, sizeof(int));
|
||||
cx_sha256_init(&G_gpg_vstate.work.md.sha256);
|
||||
cx_hash((cx_hash_t *)&G_gpg_vstate.work.md.sha256, CX_LAST, pin_val, pin_len, G_gpg_vstate.work.md.H,
|
||||
sizeof(G_gpg_vstate.work.md.H));
|
||||
if (os_memcmp(G_gpg_vstate.work.md.H, pin->value, 32)) {
|
||||
return SW_SECURITY_STATUS_NOT_SATISFIED;
|
||||
}
|
||||
|
||||
counter = 3;
|
||||
gpg_nvm_write(&(pin->counter), &counter, sizeof(int));
|
||||
return SW_OK;
|
||||
switch (pinref) {
|
||||
case PIN_ID_PW1:
|
||||
return 1;
|
||||
case PIN_ID_PW2:
|
||||
return 2;
|
||||
case PIN_ID_PW3:
|
||||
return 3;
|
||||
case PIN_ID_RC:
|
||||
return 4;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
static void gpg_pin_check_throw(gpg_pin_t *pin, int pinID, unsigned char *pin_val, int pin_len) {
|
||||
int sw;
|
||||
gpg_pin_set_verified(pinID, 0);
|
||||
sw = gpg_pin_check_internal(pin, pin_val, pin_len);
|
||||
if (sw == SW_OK) {
|
||||
gpg_pin_set_verified(pinID, 1);
|
||||
return;
|
||||
}
|
||||
THROW(sw);
|
||||
return;
|
||||
}
|
||||
/**
|
||||
* Compare the PinCode hash and handle the associated counter
|
||||
*
|
||||
* @param[in] pin PinCode reference to check
|
||||
* @param[in] pin_val PinCode value
|
||||
* @param[in] pin_len PinCode length
|
||||
*
|
||||
* @return Status Word
|
||||
*
|
||||
*/
|
||||
static int gpg_pin_check_internal(gpg_pin_t *pin, const unsigned char *pin_val, int pin_len) {
|
||||
unsigned int counter;
|
||||
cx_err_t error = CX_INTERNAL_ERROR;
|
||||
|
||||
int gpg_pin_check(gpg_pin_t *pin, int pinID, unsigned char *pin_val, unsigned int pin_len) {
|
||||
int sw;
|
||||
gpg_pin_set_verified(pinID, 0);
|
||||
sw = gpg_pin_check_internal(pin, pin_val, pin_len);
|
||||
if (sw == SW_OK) {
|
||||
gpg_pin_set_verified(pinID, 1);
|
||||
}
|
||||
return sw;
|
||||
}
|
||||
if (pin->counter == 0) {
|
||||
return SW_PIN_BLOCKED;
|
||||
}
|
||||
|
||||
void gpg_pin_set(gpg_pin_t *pin, unsigned char *pin_val, unsigned int pin_len) {
|
||||
cx_sha256_t sha256;
|
||||
counter = pin->counter - 1;
|
||||
cx_sha256_init(&G_gpg_vstate.work.md.sha256);
|
||||
CX_CHECK(cx_hash_no_throw((cx_hash_t *) &G_gpg_vstate.work.md.sha256,
|
||||
CX_LAST,
|
||||
pin_val,
|
||||
pin_len,
|
||||
G_gpg_vstate.work.md.H,
|
||||
sizeof(G_gpg_vstate.work.md.H)));
|
||||
if (memcmp(G_gpg_vstate.work.md.H, pin->value, 32)) {
|
||||
error = (counter == 0) ? SW_PIN_BLOCKED : SW_SECURITY_STATUS_NOT_SATISFIED;
|
||||
} else {
|
||||
counter = 3;
|
||||
error = SW_OK;
|
||||
}
|
||||
|
||||
gpg_pin_t newpin;
|
||||
end:
|
||||
if (counter != pin->counter) {
|
||||
nvm_write(&(pin->counter), &counter, sizeof(int));
|
||||
}
|
||||
return error;
|
||||
}
|
||||
|
||||
cx_sha256_init(&sha256);
|
||||
cx_hash((cx_hash_t *)&sha256, CX_LAST, pin_val, pin_len, newpin.value, 32);
|
||||
newpin.length = pin_len;
|
||||
newpin.counter = 3;
|
||||
/**
|
||||
* Check the PinCode value and set verification status
|
||||
*
|
||||
* @param[in] pin PinCode reference to check
|
||||
* @param[in] pin_val PinCode value
|
||||
* @param[in] pin_len PinCode length
|
||||
*
|
||||
* @return Status Word
|
||||
*
|
||||
*/
|
||||
int gpg_pin_check(gpg_pin_t *pin, int pinID, const unsigned char *pin_val, unsigned int pin_len) {
|
||||
int sw = SW_UNKNOWN;
|
||||
gpg_pin_set_verified(pinID, 0);
|
||||
sw = gpg_pin_check_internal(pin, pin_val, pin_len);
|
||||
if (sw == SW_OK) {
|
||||
gpg_pin_set_verified(pinID, 1);
|
||||
}
|
||||
return sw;
|
||||
}
|
||||
|
||||
gpg_nvm_write(pin, &newpin, sizeof(gpg_pin_t));
|
||||
/**
|
||||
* Set the PinCode value in NVRam
|
||||
*
|
||||
* @param[in] pin PinCode reference to set
|
||||
* @param[in] pin_val PinCode value
|
||||
* @param[in] pin_len PinCode length
|
||||
*
|
||||
* @return Status Word
|
||||
*
|
||||
*/
|
||||
int gpg_pin_set(gpg_pin_t *pin, unsigned char *pin_val, unsigned int pin_len) {
|
||||
cx_sha256_t sha256;
|
||||
cx_err_t error = CX_INTERNAL_ERROR;
|
||||
gpg_pin_t newpin;
|
||||
|
||||
cx_sha256_init(&sha256);
|
||||
CX_CHECK(cx_hash_no_throw((cx_hash_t *) &sha256, CX_LAST, pin_val, pin_len, newpin.value, 32));
|
||||
newpin.length = pin_len;
|
||||
newpin.counter = 3;
|
||||
|
||||
nvm_write(pin, &newpin, sizeof(gpg_pin_t));
|
||||
end:
|
||||
if (error != CX_OK) {
|
||||
return error;
|
||||
}
|
||||
return SW_OK;
|
||||
}
|
||||
|
||||
int gpg_pin_set_verified(int pinID, int verified) {
|
||||
int idx;
|
||||
idx = gpg_pin_get_state_index(pinID);
|
||||
if (idx >= 0) {
|
||||
G_gpg_vstate.verified_pin[idx] = verified;
|
||||
return verified;
|
||||
}
|
||||
return 0;
|
||||
/**
|
||||
* Change the Pin verification status
|
||||
*
|
||||
* @param[in] pinID PinCode ID to change
|
||||
* @param[in] verified new status
|
||||
*
|
||||
*/
|
||||
void gpg_pin_set_verified(int pinID, int verified) {
|
||||
int idx;
|
||||
idx = gpg_pin_get_state_index(pinID);
|
||||
if (idx >= 0) {
|
||||
G_gpg_vstate.verified_pin[idx] = verified;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the selected Pin is verified
|
||||
*
|
||||
* @param[in] pinID PinCode ID to check
|
||||
*
|
||||
* @return 0 or 1 (false or true)
|
||||
*
|
||||
*/
|
||||
int gpg_pin_is_verified(int pinID) {
|
||||
int idx;
|
||||
idx = gpg_pin_get_state_index(pinID);
|
||||
if (idx >= 0) {
|
||||
return G_gpg_vstate.verified_pin[idx];
|
||||
}
|
||||
return 0;
|
||||
int idx;
|
||||
idx = gpg_pin_get_state_index(pinID);
|
||||
if (idx >= 0) {
|
||||
return G_gpg_vstate.verified_pin[idx];
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the selected Pin is blocked
|
||||
*
|
||||
* @param[in] pin PinCode reference to check
|
||||
*
|
||||
* @return 0 or 1 (false or true)
|
||||
*
|
||||
*/
|
||||
int gpg_pin_is_blocked(gpg_pin_t *pin) {
|
||||
return pin->counter == 0;
|
||||
return pin->counter == 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* APDU handler to Verify PinCode
|
||||
*
|
||||
* @return Status Word
|
||||
*
|
||||
*/
|
||||
int gpg_apdu_verify() {
|
||||
gpg_pin_t *pin;
|
||||
int sw = SW_UNKNOWN;
|
||||
gpg_pin_t *pin;
|
||||
|
||||
pin = gpg_pin_get_pin(G_gpg_vstate.io_p2);
|
||||
if (pin == NULL) {
|
||||
THROW(SW_WRONG_DATA);
|
||||
return SW_WRONG_DATA;
|
||||
}
|
||||
|
||||
// PINPAD
|
||||
if (G_gpg_vstate.io_cla == 0xEF) {
|
||||
if (gpg_pin_is_blocked(pin)) {
|
||||
THROW(SW_PIN_BLOCKED);
|
||||
return SW_PIN_BLOCKED;
|
||||
pin = gpg_pin_get_pin(G_gpg_vstate.io_p2);
|
||||
if (pin == NULL) {
|
||||
return SW_WRONG_DATA;
|
||||
}
|
||||
|
||||
if (G_gpg_vstate.pinmode == PIN_MODE_SCREEN) {
|
||||
// Delegate pin check to ui
|
||||
gpg_io_discard(1);
|
||||
ui_menu_pinentry_display(0);
|
||||
return 0;
|
||||
}
|
||||
if (G_gpg_vstate.pinmode == PIN_MODE_CONFIRM) {
|
||||
// Delegate pin check to ui
|
||||
gpg_io_discard(1);
|
||||
ui_menu_pinconfirm_display(0);
|
||||
return 0;
|
||||
// PINPAD
|
||||
if (G_gpg_vstate.io_cla == CLA_APP_APDU_PIN) {
|
||||
if (gpg_pin_is_blocked(pin)) {
|
||||
return SW_PIN_BLOCKED;
|
||||
}
|
||||
|
||||
switch (G_gpg_vstate.pinmode) {
|
||||
case PIN_MODE_SCREEN:
|
||||
// Delegate pin check to ui
|
||||
gpg_io_discard(1);
|
||||
ui_menu_pinentry_display(0);
|
||||
sw = 0;
|
||||
break;
|
||||
case PIN_MODE_CONFIRM:
|
||||
// Delegate pin check to ui
|
||||
gpg_io_discard(1);
|
||||
ui_menu_pinconfirm_display(G_gpg_vstate.io_p2);
|
||||
sw = 0;
|
||||
break;
|
||||
case PIN_MODE_TRUST:
|
||||
gpg_pin_set_verified(G_gpg_vstate.io_p2, 1);
|
||||
gpg_io_discard(1);
|
||||
sw = 0;
|
||||
break;
|
||||
default:
|
||||
sw = SW_WRONG_DATA;
|
||||
break;
|
||||
}
|
||||
return sw;
|
||||
}
|
||||
if (G_gpg_vstate.pinmode == PIN_MODE_TRUST) {
|
||||
gpg_pin_set_verified(G_gpg_vstate.io_p2, 1);
|
||||
gpg_io_discard(1);
|
||||
return SW_OK;
|
||||
}
|
||||
THROW(SW_WRONG_DATA);
|
||||
return SW_WRONG_DATA;
|
||||
}
|
||||
|
||||
// NORMAL CHECK
|
||||
if ((G_gpg_vstate.io_p1 == 0) && G_gpg_vstate.io_length) {
|
||||
if (gpg_pin_is_blocked(pin)) {
|
||||
THROW(SW_PIN_BLOCKED);
|
||||
return SW_PIN_BLOCKED;
|
||||
// NORMAL CHECK
|
||||
if ((G_gpg_vstate.io_p1 == PIN_VERIFY) && G_gpg_vstate.io_length) {
|
||||
if (gpg_pin_is_blocked(pin)) {
|
||||
return SW_PIN_BLOCKED;
|
||||
}
|
||||
sw = gpg_pin_check(pin,
|
||||
G_gpg_vstate.io_p2,
|
||||
G_gpg_vstate.work.io_buffer + G_gpg_vstate.io_offset,
|
||||
G_gpg_vstate.io_length);
|
||||
gpg_io_discard(1);
|
||||
return sw;
|
||||
}
|
||||
gpg_pin_check_throw(pin, G_gpg_vstate.io_p2, G_gpg_vstate.work.io_buffer + G_gpg_vstate.io_offset,
|
||||
G_gpg_vstate.io_length);
|
||||
gpg_io_discard(1);
|
||||
return SW_OK;
|
||||
}
|
||||
|
||||
gpg_io_discard(1);
|
||||
gpg_io_discard(1);
|
||||
|
||||
// STATUS REQUEST
|
||||
if ((G_gpg_vstate.io_p1 == 0) && G_gpg_vstate.io_length == 0) {
|
||||
if (gpg_pin_is_verified(G_gpg_vstate.io_p2)) {
|
||||
return SW_OK;
|
||||
// STATUS REQUEST
|
||||
if ((G_gpg_vstate.io_p1 == PIN_VERIFY) && G_gpg_vstate.io_length == 0) {
|
||||
if (gpg_pin_is_verified(G_gpg_vstate.io_p2)) {
|
||||
return SW_OK;
|
||||
}
|
||||
return SW_PWD_NOT_CHECKED | pin->counter;
|
||||
}
|
||||
return 0x63C0 | pin->counter;
|
||||
}
|
||||
|
||||
// RESET REQUEST
|
||||
if ((G_gpg_vstate.io_p1 == 0xFF) && G_gpg_vstate.io_length == 0) {
|
||||
gpg_pin_set_verified(G_gpg_vstate.io_p2, 0);
|
||||
return SW_OK;
|
||||
}
|
||||
// RESET REQUEST
|
||||
if ((G_gpg_vstate.io_p1 == PIN_NOT_VERIFIED) && G_gpg_vstate.io_length == 0) {
|
||||
gpg_pin_set_verified(G_gpg_vstate.io_p2, 0);
|
||||
return SW_OK;
|
||||
}
|
||||
|
||||
THROW(SW_WRONG_DATA);
|
||||
return SW_WRONG_DATA;
|
||||
return SW_WRONG_DATA;
|
||||
}
|
||||
|
||||
/**
|
||||
* APDU handler to Change PinCode
|
||||
*
|
||||
* @return Status Word
|
||||
*
|
||||
*/
|
||||
int gpg_apdu_change_ref_data() {
|
||||
gpg_pin_t *pin;
|
||||
int len, newlen;
|
||||
int sw = SW_UNKNOWN;
|
||||
gpg_pin_t *pin;
|
||||
int len, newlen;
|
||||
|
||||
pin = gpg_pin_get_pin(G_gpg_vstate.io_p2);
|
||||
if (pin == NULL) {
|
||||
THROW(SW_WRONG_DATA);
|
||||
return SW_WRONG_DATA;
|
||||
}
|
||||
|
||||
gpg_pin_set_verified(pin->ref, 0);
|
||||
|
||||
// --- PW1/PW3 pin ---
|
||||
if (gpg_pin_is_blocked(pin)) {
|
||||
THROW(SW_PIN_BLOCKED);
|
||||
return SW_PIN_BLOCKED;
|
||||
}
|
||||
// avoid any-overflow whitout giving info
|
||||
if (G_gpg_vstate.io_length == 0) {
|
||||
if (G_gpg_vstate.pinmode != PIN_MODE_HOST) {
|
||||
// Delegate pin change to ui
|
||||
gpg_io_discard(1);
|
||||
ui_menu_pinentry_display(0);
|
||||
return 0;
|
||||
pin = gpg_pin_get_pin(G_gpg_vstate.io_p2);
|
||||
if (pin == NULL) {
|
||||
return SW_WRONG_DATA;
|
||||
}
|
||||
}
|
||||
|
||||
if (pin->length > G_gpg_vstate.io_length) {
|
||||
len = G_gpg_vstate.io_length;
|
||||
} else {
|
||||
len = pin->length;
|
||||
}
|
||||
gpg_pin_set_verified(pin->ref, 0);
|
||||
|
||||
gpg_pin_check_throw(pin, pin->ref, G_gpg_vstate.work.io_buffer + G_gpg_vstate.io_offset, len);
|
||||
// --- PW1/PW3 pin ---
|
||||
if (gpg_pin_is_blocked(pin)) {
|
||||
return SW_PIN_BLOCKED;
|
||||
}
|
||||
// avoid any-overflow without giving info
|
||||
if (G_gpg_vstate.io_length == 0) {
|
||||
// Delegate pin change to ui
|
||||
gpg_io_discard(1);
|
||||
ui_menu_pinentry_display(0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
newlen = G_gpg_vstate.io_length - len;
|
||||
if ((newlen > GPG_MAX_PW_LENGTH) || ((pin->ref == PIN_ID_PW1) && (newlen < 6)) ||
|
||||
((pin->ref == PIN_ID_PW3) && (newlen < 8))) {
|
||||
THROW(SW_WRONG_DATA);
|
||||
return SW_WRONG_DATA;
|
||||
}
|
||||
gpg_pin_set(pin, G_gpg_vstate.work.io_buffer + G_gpg_vstate.io_offset + len, newlen);
|
||||
gpg_io_discard(1);
|
||||
return SW_OK;
|
||||
len = MIN(G_gpg_vstate.io_length, pin->length);
|
||||
sw = gpg_pin_check(pin, pin->ref, G_gpg_vstate.work.io_buffer + G_gpg_vstate.io_offset, len);
|
||||
if (sw != SW_OK) {
|
||||
return sw;
|
||||
}
|
||||
|
||||
newlen = G_gpg_vstate.io_length - len;
|
||||
if ((newlen > GPG_MAX_PW_LENGTH) ||
|
||||
((pin->ref == PIN_ID_PW1) && (newlen < GPG_MIN_PW1_LENGTH)) ||
|
||||
((pin->ref == PIN_ID_PW3) && (newlen < GPG_MIN_PW3_LENGTH))) {
|
||||
return SW_WRONG_DATA;
|
||||
}
|
||||
sw = gpg_pin_set(pin, G_gpg_vstate.work.io_buffer + G_gpg_vstate.io_offset + len, newlen);
|
||||
gpg_io_discard(1);
|
||||
return sw;
|
||||
}
|
||||
|
||||
/**
|
||||
* APDU handler to Reste PinCode or Counter
|
||||
*
|
||||
* @return Status Word
|
||||
*
|
||||
*/
|
||||
int gpg_apdu_reset_retry_counter() {
|
||||
gpg_pin_t *pin_pw1;
|
||||
gpg_pin_t *pin_pw3;
|
||||
gpg_pin_t *pin_rc;
|
||||
int rc_len, pw1_len;
|
||||
|
||||
pin_pw1 = gpg_pin_get_pin(PIN_ID_PW1);
|
||||
pin_pw3 = gpg_pin_get_pin(PIN_ID_PW3);
|
||||
pin_rc = gpg_pin_get_pin(PIN_ID_RC);
|
||||
|
||||
if (G_gpg_vstate.io_p1 == 2) {
|
||||
if (!gpg_pin_is_verified(PIN_ID_PW3)) {
|
||||
THROW(SW_SECURITY_STATUS_NOT_SATISFIED);
|
||||
return SW_SECURITY_STATUS_NOT_SATISFIED;
|
||||
}
|
||||
rc_len = 0;
|
||||
pw1_len = G_gpg_vstate.io_length;
|
||||
} else {
|
||||
// avoid any-overflow whitout giving info
|
||||
if (pin_rc->length > G_gpg_vstate.io_length) {
|
||||
rc_len = G_gpg_vstate.io_length;
|
||||
int sw = SW_UNKNOWN;
|
||||
gpg_pin_t *pin_pw1;
|
||||
gpg_pin_t *pin_rc;
|
||||
int rc_len, pw1_len;
|
||||
|
||||
pin_pw1 = gpg_pin_get_pin(PIN_ID_PW1);
|
||||
pin_rc = gpg_pin_get_pin(PIN_ID_RC);
|
||||
|
||||
if (G_gpg_vstate.io_p1 == RESET_RETRY_WITH_PW3) {
|
||||
// PW3 must be verified, and the data contain the new PW1
|
||||
if (!gpg_pin_is_verified(PIN_ID_PW3)) {
|
||||
return SW_SECURITY_STATUS_NOT_SATISFIED;
|
||||
}
|
||||
rc_len = 0;
|
||||
pw1_len = G_gpg_vstate.io_length;
|
||||
} else {
|
||||
rc_len = pin_rc->length;
|
||||
// The data contain the Resetting Code + the new PW1
|
||||
// avoid any-overflow without giving info
|
||||
rc_len = MIN(G_gpg_vstate.io_length, pin_rc->length);
|
||||
pw1_len = G_gpg_vstate.io_length - rc_len;
|
||||
sw = gpg_pin_check(pin_rc,
|
||||
pin_rc->ref,
|
||||
G_gpg_vstate.work.io_buffer + G_gpg_vstate.io_offset,
|
||||
rc_len);
|
||||
if (sw != SW_OK) {
|
||||
return sw;
|
||||
}
|
||||
}
|
||||
pw1_len = G_gpg_vstate.io_length - rc_len;
|
||||
gpg_pin_check_throw(pin_rc, pin_rc->ref, G_gpg_vstate.work.io_buffer + G_gpg_vstate.io_offset, rc_len);
|
||||
}
|
||||
|
||||
if ((pw1_len > GPG_MAX_PW_LENGTH) || (pw1_len < 6)) {
|
||||
THROW(SW_WRONG_DATA);
|
||||
return SW_WRONG_DATA;
|
||||
}
|
||||
gpg_pin_set(pin_pw1, G_gpg_vstate.work.io_buffer + G_gpg_vstate.io_offset + rc_len, pw1_len);
|
||||
gpg_io_discard(1);
|
||||
return SW_OK;
|
||||
if ((pw1_len > GPG_MAX_PW_LENGTH) || (pw1_len < GPG_MIN_PW1_LENGTH)) {
|
||||
return SW_WRONG_DATA;
|
||||
}
|
||||
sw = gpg_pin_set(pin_pw1,
|
||||
G_gpg_vstate.work.io_buffer + G_gpg_vstate.io_offset + rc_len,
|
||||
pw1_len);
|
||||
gpg_io_discard(1);
|
||||
return sw;
|
||||
}
|
||||
|
@ -1,338 +1,433 @@
|
||||
/* Copyright 2017 Cedric Mesnil <cslashm@gmail.com>, Ledger SAS
|
||||
/*****************************************************************************
|
||||
* Ledger App OpenPGP.
|
||||
* (c) 2024 Ledger SAS.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*****************************************************************************/
|
||||
|
||||
#include "os.h"
|
||||
#include "cx.h"
|
||||
#include "gpg_types.h"
|
||||
#include "gpg_api.h"
|
||||
#include "gpg_vars.h"
|
||||
#include "gpg_ux_nanos.h"
|
||||
#include "gpg_ux.h"
|
||||
#include "cx_errors.h"
|
||||
|
||||
const unsigned char gpg_oid_sha256[] = {0x30, 0x31, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01,
|
||||
0x65, 0x03, 0x04, 0x02, 0x01, 0x05, 0x00, 0x04, 0x20};
|
||||
const unsigned char gpg_oid_sha512[] = {0x30, 0x51, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01,
|
||||
0x65, 0x03, 0x04, 0x02, 0x03, 0x05, 0x00, 0x04, 0x40};
|
||||
const unsigned char gpg_oid_sha256[] = {0x30,
|
||||
0x31,
|
||||
0x30,
|
||||
0x0d,
|
||||
0x06,
|
||||
0x09,
|
||||
0x60,
|
||||
0x86,
|
||||
0x48,
|
||||
0x01,
|
||||
0x65,
|
||||
0x03,
|
||||
0x04,
|
||||
0x02,
|
||||
0x01,
|
||||
0x05,
|
||||
0x00,
|
||||
0x04,
|
||||
0x20};
|
||||
const unsigned char gpg_oid_sha512[] = {0x30,
|
||||
0x51,
|
||||
0x30,
|
||||
0x0d,
|
||||
0x06,
|
||||
0x09,
|
||||
0x60,
|
||||
0x86,
|
||||
0x48,
|
||||
0x01,
|
||||
0x65,
|
||||
0x03,
|
||||
0x04,
|
||||
0x02,
|
||||
0x03,
|
||||
0x05,
|
||||
0x00,
|
||||
0x04,
|
||||
0x40};
|
||||
|
||||
/**
|
||||
* Reset PW1 verified status
|
||||
*
|
||||
*/
|
||||
static void gpg_pso_reset_PW1() {
|
||||
if (N_gpg_pstate->PW_status[0] == 0) {
|
||||
gpg_pin_set_verified(PIN_ID_PW1, 0);
|
||||
}
|
||||
if (N_gpg_pstate->PW_status[0] == 0) {
|
||||
gpg_pin_set_verified(PIN_ID_PW1, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a Digital Signature
|
||||
*
|
||||
* @param[in] sigKey signing key
|
||||
*
|
||||
* @return Status Word
|
||||
*
|
||||
*/
|
||||
static int gpg_sign(gpg_key_t *sigkey) {
|
||||
// --- RSA
|
||||
if (sigkey->attributes.value[0] == 1) {
|
||||
cx_rsa_private_key_t *key;
|
||||
unsigned int ksz, l;
|
||||
ksz = (sigkey->attributes.value[1] << 8) | sigkey->attributes.value[2];
|
||||
ksz = ksz >> 3;
|
||||
switch (ksz) {
|
||||
case 1024 / 8:
|
||||
key = (cx_rsa_private_key_t *)&sigkey->priv_key.rsa1024;
|
||||
break;
|
||||
case 2048 / 8:
|
||||
key = (cx_rsa_private_key_t *)&sigkey->priv_key.rsa2048;
|
||||
break;
|
||||
case 3072 / 8:
|
||||
key = (cx_rsa_private_key_t *)&sigkey->priv_key.rsa3072;
|
||||
break;
|
||||
case 4096 / 8:
|
||||
key = (cx_rsa_private_key_t *)&sigkey->priv_key.rsa4096;
|
||||
break;
|
||||
}
|
||||
if (key->size != ksz) {
|
||||
THROW(SW_CONDITIONS_NOT_SATISFIED);
|
||||
return SW_CONDITIONS_NOT_SATISFIED;
|
||||
}
|
||||
cx_err_t error = CX_INTERNAL_ERROR;
|
||||
cx_rsa_private_key_t *rsa_key = NULL;
|
||||
unsigned int ksz, l;
|
||||
cx_ecfp_private_key_t *ecfp_key = NULL;
|
||||
unsigned int s_len, i, rs_len, info;
|
||||
unsigned char *rs;
|
||||
|
||||
// sign
|
||||
if (ksz < G_gpg_vstate.io_length) {
|
||||
THROW(SW_WRONG_LENGTH);
|
||||
}
|
||||
l = ksz - G_gpg_vstate.io_length;
|
||||
os_memmove(G_gpg_vstate.work.io_buffer + l, G_gpg_vstate.work.io_buffer, G_gpg_vstate.io_length);
|
||||
os_memset(G_gpg_vstate.work.io_buffer, 0xFF, l);
|
||||
G_gpg_vstate.work.io_buffer[0] = 0;
|
||||
G_gpg_vstate.work.io_buffer[1] = 1;
|
||||
G_gpg_vstate.work.io_buffer[l - 1] = 0;
|
||||
ksz = cx_rsa_decrypt(key, CX_PAD_NONE, CX_NONE, G_gpg_vstate.work.io_buffer, ksz, G_gpg_vstate.work.io_buffer, ksz);
|
||||
// send
|
||||
gpg_io_discard(0);
|
||||
gpg_io_inserted(ksz);
|
||||
gpg_pso_reset_PW1();
|
||||
return SW_OK;
|
||||
}
|
||||
// --- ECDSA/EdDSA
|
||||
if ((sigkey->attributes.value[0] == 19) || (sigkey->attributes.value[0] == 22)) {
|
||||
cx_ecfp_private_key_t *key;
|
||||
unsigned int sz, i, rs_len, info;
|
||||
unsigned char * rs;
|
||||
switch (sigkey->attributes.value[0]) {
|
||||
case KEY_ID_RSA:
|
||||
ksz = U2BE(sigkey->attributes.value, 1) >> 3;
|
||||
switch (ksz) {
|
||||
case 2048 / 8:
|
||||
rsa_key = (cx_rsa_private_key_t *) &sigkey->priv_key.rsa2048;
|
||||
break;
|
||||
case 3072 / 8:
|
||||
rsa_key = (cx_rsa_private_key_t *) &sigkey->priv_key.rsa3072;
|
||||
break;
|
||||
#ifdef WITH_SUPPORT_RSA4096
|
||||
case 4096 / 8:
|
||||
rsa_key = (cx_rsa_private_key_t *) &sigkey->priv_key.rsa4096;
|
||||
break;
|
||||
#endif
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if ((rsa_key == NULL) || (rsa_key->size != ksz)) {
|
||||
error = SW_CONDITIONS_NOT_SATISFIED;
|
||||
break;
|
||||
}
|
||||
|
||||
key = &sigkey->priv_key.ecfp;
|
||||
// sign
|
||||
if (ksz < G_gpg_vstate.io_length) {
|
||||
error = SW_WRONG_LENGTH;
|
||||
break;
|
||||
}
|
||||
l = ksz - G_gpg_vstate.io_length;
|
||||
memmove(G_gpg_vstate.work.io_buffer + l,
|
||||
G_gpg_vstate.work.io_buffer,
|
||||
G_gpg_vstate.io_length);
|
||||
memset(G_gpg_vstate.work.io_buffer, 0xFF, l);
|
||||
G_gpg_vstate.work.io_buffer[0] = 0;
|
||||
G_gpg_vstate.work.io_buffer[1] = 1;
|
||||
G_gpg_vstate.work.io_buffer[l - 1] = 0;
|
||||
CX_CHECK(cx_rsa_decrypt_no_throw(rsa_key,
|
||||
CX_PAD_NONE,
|
||||
CX_NONE,
|
||||
G_gpg_vstate.work.io_buffer,
|
||||
ksz,
|
||||
G_gpg_vstate.work.io_buffer,
|
||||
&ksz));
|
||||
// send
|
||||
gpg_io_discard(0);
|
||||
gpg_io_inserted(ksz);
|
||||
gpg_pso_reset_PW1();
|
||||
error = SW_OK;
|
||||
break;
|
||||
|
||||
// sign
|
||||
#define RS (G_gpg_vstate.work.io_buffer + (GPG_IO_BUFFER_LENGTH - 256))
|
||||
if (sigkey->attributes.value[0] == 19) {
|
||||
sz = gpg_curve2domainlen(key->curve);
|
||||
if ((sz == 0) || (key->d_len != sz)) {
|
||||
THROW(SW_CONDITIONS_NOT_SATISFIED);
|
||||
return SW_CONDITIONS_NOT_SATISFIED;
|
||||
}
|
||||
sz = cx_ecdsa_sign(key, CX_RND_TRNG, CX_NONE, G_gpg_vstate.work.io_buffer, sz, RS, 256, &info);
|
||||
// reencode r,s in MPI format
|
||||
gpg_io_discard(0);
|
||||
case KEY_ID_ECDSA:
|
||||
ecfp_key = &sigkey->priv_key.ecfp;
|
||||
ksz = (unsigned int) gpg_curve2domainlen(ecfp_key->curve);
|
||||
if ((ksz == 0) || (ecfp_key->d_len != ksz)) {
|
||||
error = SW_CONDITIONS_NOT_SATISFIED;
|
||||
break;
|
||||
}
|
||||
s_len = 256;
|
||||
CX_CHECK(cx_ecdsa_sign_no_throw(ecfp_key,
|
||||
CX_RND_TRNG,
|
||||
CX_NONE,
|
||||
G_gpg_vstate.work.io_buffer,
|
||||
ksz,
|
||||
RS,
|
||||
&s_len,
|
||||
&info));
|
||||
// re-encode r,s in MPI format
|
||||
gpg_io_discard(0);
|
||||
|
||||
rs_len = RS[3];
|
||||
rs = &RS[4];
|
||||
rs_len = RS[3];
|
||||
rs = &RS[4];
|
||||
|
||||
for (i = 0; i < 2; i++) {
|
||||
if (*rs == 0) {
|
||||
rs++;
|
||||
rs_len--;
|
||||
}
|
||||
gpg_io_insert_u8(0);
|
||||
gpg_io_insert(rs, rs_len);
|
||||
rs = rs + rs_len;
|
||||
rs_len = rs[1];
|
||||
rs += 2;
|
||||
}
|
||||
} else {
|
||||
sz = cx_eddsa_sign(key, CX_NONE, CX_SHA512, G_gpg_vstate.work.io_buffer, G_gpg_vstate.io_length, NULL, 0, RS, 256,
|
||||
NULL);
|
||||
gpg_io_discard(0);
|
||||
gpg_io_insert(RS, sz);
|
||||
for (i = 0; i < 2; i++) {
|
||||
if (*rs == 0) {
|
||||
rs++;
|
||||
rs_len--;
|
||||
}
|
||||
gpg_io_insert_u8(0);
|
||||
gpg_io_insert(rs, rs_len);
|
||||
rs = rs + rs_len;
|
||||
rs_len = rs[1];
|
||||
rs += 2;
|
||||
}
|
||||
error = SW_OK;
|
||||
break;
|
||||
|
||||
case KEY_ID_EDDSA:
|
||||
ecfp_key = &sigkey->priv_key.ecfp;
|
||||
ksz = 256;
|
||||
CX_CHECK(cx_eddsa_sign_no_throw(ecfp_key,
|
||||
CX_SHA512,
|
||||
G_gpg_vstate.work.io_buffer,
|
||||
G_gpg_vstate.io_length,
|
||||
RS,
|
||||
ksz));
|
||||
CX_CHECK(cx_ecdomain_parameters_length(ecfp_key->curve, &ksz));
|
||||
ksz *= 2;
|
||||
gpg_io_discard(0);
|
||||
gpg_io_insert(RS, ksz);
|
||||
error = SW_OK;
|
||||
break;
|
||||
|
||||
default:
|
||||
// --- PSO:CDS NOT SUPPORTED
|
||||
error = SW_REFERENCED_DATA_NOT_FOUND;
|
||||
break;
|
||||
}
|
||||
#undef RS
|
||||
|
||||
// send
|
||||
end:
|
||||
gpg_pso_reset_PW1();
|
||||
return SW_OK;
|
||||
}
|
||||
// --- PSO:CDS NOT SUPPORTED
|
||||
THROW(SW_REFERENCED_DATA_NOT_FOUND);
|
||||
return SW_REFERENCED_DATA_NOT_FOUND;
|
||||
return error;
|
||||
}
|
||||
|
||||
/**
|
||||
* APDU handler to Perform Security Operation
|
||||
*
|
||||
* @return Status Word
|
||||
*
|
||||
*/
|
||||
int gpg_apdu_pso() {
|
||||
unsigned int t, l, ksz;
|
||||
cx_err_t error = CX_INTERNAL_ERROR;
|
||||
unsigned int t, l, ksz;
|
||||
unsigned int cnt, pad_byte;
|
||||
unsigned int msg_len;
|
||||
unsigned int curve;
|
||||
cx_aes_key_t *aes_key = NULL;
|
||||
cx_rsa_private_key_t *rsa_key = NULL;
|
||||
cx_ecfp_private_key_t *ecfp_key = NULL;
|
||||
|
||||
unsigned int pso;
|
||||
// UIF HANDLE
|
||||
switch (G_gpg_vstate.io_p1p2) {
|
||||
// --- PSO:CDS ---
|
||||
case PSO_CDS:
|
||||
if (G_gpg_vstate.kslot->sig.UIF[0]) {
|
||||
if ((G_gpg_vstate.UIF_flags) == 0) {
|
||||
ui_menu_uifconfirm_display(0);
|
||||
return 0;
|
||||
}
|
||||
G_gpg_vstate.UIF_flags = 0;
|
||||
}
|
||||
break;
|
||||
// --- PSO:DEC ---
|
||||
case PSO_DEC:
|
||||
case PSO_ENC:
|
||||
if (G_gpg_vstate.kslot->dec.UIF[0]) {
|
||||
if ((G_gpg_vstate.UIF_flags) == 0) {
|
||||
ui_menu_uifconfirm_display(0);
|
||||
return 0;
|
||||
}
|
||||
G_gpg_vstate.UIF_flags = 0;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
pso = (G_gpg_vstate.io_p1 << 8) | G_gpg_vstate.io_p2;
|
||||
// --- PSO:ENC ---
|
||||
switch (G_gpg_vstate.io_p1p2) {
|
||||
case PSO_CDS:
|
||||
error = gpg_sign(&G_gpg_vstate.kslot->sig);
|
||||
cnt = G_gpg_vstate.kslot->sig_count + 1;
|
||||
nvm_write(&G_gpg_vstate.kslot->sig_count, &cnt, sizeof(unsigned int));
|
||||
break;
|
||||
|
||||
// UIF HANDLE
|
||||
switch (pso) {
|
||||
// --- PSO:CDS ---
|
||||
case 0x9e9a:
|
||||
if (G_gpg_vstate.kslot->sig.UIF[0]) {
|
||||
if ((G_gpg_vstate.UIF_flags) == 0) {
|
||||
ui_menu_uifconfirm_display(0);
|
||||
return 0;
|
||||
}
|
||||
G_gpg_vstate.UIF_flags = 0;
|
||||
}
|
||||
break;
|
||||
// --- PSO:DEC ---
|
||||
case 0x8086:
|
||||
case 0x8680:
|
||||
if (G_gpg_vstate.kslot->dec.UIF[0]) {
|
||||
if ((G_gpg_vstate.UIF_flags) == 0) {
|
||||
ui_menu_uifconfirm_display(0);
|
||||
return 0;
|
||||
}
|
||||
G_gpg_vstate.UIF_flags = 0;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case PSO_ENC:
|
||||
aes_key = &G_gpg_vstate.kslot->AES_dec;
|
||||
if (!(aes_key->size != CX_AES_128_KEY_LEN)) {
|
||||
return SW_CONDITIONS_NOT_SATISFIED;
|
||||
}
|
||||
msg_len = G_gpg_vstate.io_length - G_gpg_vstate.io_offset;
|
||||
ksz = GPG_IO_BUFFER_LENGTH - 1;
|
||||
CX_CHECK(cx_aes_no_throw(aes_key,
|
||||
CX_ENCRYPT | CX_CHAIN_CBC | CX_LAST,
|
||||
G_gpg_vstate.work.io_buffer + G_gpg_vstate.io_offset,
|
||||
msg_len,
|
||||
G_gpg_vstate.work.io_buffer + 1,
|
||||
&ksz));
|
||||
// send
|
||||
gpg_io_discard(0);
|
||||
G_gpg_vstate.work.io_buffer[0] = PAD_AES;
|
||||
gpg_io_inserted(1 + ksz);
|
||||
error = SW_OK;
|
||||
break;
|
||||
|
||||
// --- PSO:ENC ---
|
||||
switch (pso) {
|
||||
// --- PSO:CDS ---
|
||||
case 0x9e9a: {
|
||||
unsigned int cnt;
|
||||
int sw;
|
||||
sw = gpg_sign(&G_gpg_vstate.kslot->sig);
|
||||
cnt = G_gpg_vstate.kslot->sig_count + 1;
|
||||
nvm_write(&G_gpg_vstate.kslot->sig_count, &cnt, sizeof(unsigned int));
|
||||
return sw;
|
||||
}
|
||||
// --- PSO:ENC ---
|
||||
case 0x8680: {
|
||||
unsigned int msg_len;
|
||||
cx_aes_key_t *key;
|
||||
unsigned int sz;
|
||||
key = &G_gpg_vstate.kslot->AES_dec;
|
||||
if (!(key->size != 16)) {
|
||||
THROW(SW_CONDITIONS_NOT_SATISFIED + 5);
|
||||
return SW_CONDITIONS_NOT_SATISFIED;
|
||||
}
|
||||
msg_len = G_gpg_vstate.io_length - G_gpg_vstate.io_offset;
|
||||
sz = cx_aes(key, CX_ENCRYPT | CX_CHAIN_CBC | CX_LAST, G_gpg_vstate.work.io_buffer + G_gpg_vstate.io_offset, msg_len,
|
||||
G_gpg_vstate.work.io_buffer + 1, GPG_IO_BUFFER_LENGTH - 1);
|
||||
// send
|
||||
gpg_io_discard(0);
|
||||
G_gpg_vstate.work.io_buffer[0] = 0x02;
|
||||
gpg_io_inserted(1 + sz);
|
||||
return SW_OK;
|
||||
}
|
||||
case PSO_DEC:
|
||||
pad_byte = gpg_io_fetch_u8();
|
||||
|
||||
// --- PSO:DEC ---
|
||||
case 0x8086: {
|
||||
unsigned int msg_len;
|
||||
unsigned int pad_byte;
|
||||
unsigned int sz;
|
||||
pad_byte = gpg_io_fetch_u8();
|
||||
switch (pad_byte) {
|
||||
case PAD_RSA:
|
||||
if (G_gpg_vstate.mse_dec->attributes.value[0] != KEY_ID_RSA) {
|
||||
error = SW_CONDITIONS_NOT_SATISFIED;
|
||||
break;
|
||||
}
|
||||
ksz = U2BE(G_gpg_vstate.mse_dec->attributes.value, 1) >> 3;
|
||||
switch (ksz) {
|
||||
case 2048 / 8:
|
||||
rsa_key =
|
||||
(cx_rsa_private_key_t *) &G_gpg_vstate.mse_dec->priv_key.rsa2048;
|
||||
break;
|
||||
case 3072 / 8:
|
||||
rsa_key =
|
||||
(cx_rsa_private_key_t *) &G_gpg_vstate.mse_dec->priv_key.rsa3072;
|
||||
break;
|
||||
#ifdef WITH_SUPPORT_RSA4096
|
||||
case 4096 / 8:
|
||||
rsa_key =
|
||||
(cx_rsa_private_key_t *) &G_gpg_vstate.mse_dec->priv_key.rsa4096;
|
||||
break;
|
||||
#endif
|
||||
}
|
||||
|
||||
switch (pad_byte) {
|
||||
// --- PSO:DEC:RSA
|
||||
case 0x00: {
|
||||
cx_rsa_private_key_t *key;
|
||||
if (G_gpg_vstate.mse_dec->attributes.value[0] != 0x01) {
|
||||
THROW(SW_CONDITIONS_NOT_SATISFIED);
|
||||
return SW_CONDITIONS_NOT_SATISFIED;
|
||||
}
|
||||
ksz = (G_gpg_vstate.mse_dec->attributes.value[1] << 8) | G_gpg_vstate.mse_dec->attributes.value[2];
|
||||
ksz = ksz >> 3;
|
||||
key = NULL;
|
||||
switch (ksz) {
|
||||
case 1024 / 8:
|
||||
key = (cx_rsa_private_key_t *)&G_gpg_vstate.mse_dec->priv_key.rsa1024;
|
||||
break;
|
||||
case 2048 / 8:
|
||||
key = (cx_rsa_private_key_t *)&G_gpg_vstate.mse_dec->priv_key.rsa2048;
|
||||
break;
|
||||
case 3072 / 8:
|
||||
key = (cx_rsa_private_key_t *)&G_gpg_vstate.mse_dec->priv_key.rsa3072;
|
||||
break;
|
||||
case 4096 / 8:
|
||||
key = (cx_rsa_private_key_t *)&G_gpg_vstate.mse_dec->priv_key.rsa4096;
|
||||
break;
|
||||
}
|
||||
if ((rsa_key == NULL) || (rsa_key->size != ksz)) {
|
||||
error = SW_CONDITIONS_NOT_SATISFIED;
|
||||
break;
|
||||
}
|
||||
msg_len = G_gpg_vstate.io_length - G_gpg_vstate.io_offset;
|
||||
CX_CHECK(cx_rsa_decrypt_no_throw(
|
||||
rsa_key,
|
||||
CX_PAD_PKCS1_1o5,
|
||||
CX_NONE,
|
||||
G_gpg_vstate.work.io_buffer + G_gpg_vstate.io_offset,
|
||||
msg_len,
|
||||
G_gpg_vstate.work.io_buffer,
|
||||
&ksz));
|
||||
// send
|
||||
gpg_io_discard(0);
|
||||
gpg_io_inserted(ksz);
|
||||
error = SW_OK;
|
||||
break;
|
||||
|
||||
if ((key == NULL) || (key->size != ksz)) {
|
||||
THROW(SW_CONDITIONS_NOT_SATISFIED);
|
||||
return SW_CONDITIONS_NOT_SATISFIED;
|
||||
}
|
||||
msg_len = G_gpg_vstate.io_length - G_gpg_vstate.io_offset;
|
||||
sz = cx_rsa_decrypt(key, CX_PAD_PKCS1_1o5, CX_NONE, G_gpg_vstate.work.io_buffer + G_gpg_vstate.io_offset, msg_len,
|
||||
G_gpg_vstate.work.io_buffer, ksz);
|
||||
// send
|
||||
gpg_io_discard(0);
|
||||
gpg_io_inserted(sz);
|
||||
return SW_OK;
|
||||
}
|
||||
case PAD_AES:
|
||||
aes_key = &G_gpg_vstate.kslot->AES_dec;
|
||||
if (!(aes_key->size != CX_AES_128_KEY_LEN)) {
|
||||
error = SW_CONDITIONS_NOT_SATISFIED;
|
||||
break;
|
||||
}
|
||||
msg_len = G_gpg_vstate.io_length - G_gpg_vstate.io_offset;
|
||||
ksz = GPG_IO_BUFFER_LENGTH;
|
||||
CX_CHECK(cx_aes_no_throw(aes_key,
|
||||
CX_DECRYPT | CX_CHAIN_CBC | CX_LAST,
|
||||
G_gpg_vstate.work.io_buffer + G_gpg_vstate.io_offset,
|
||||
msg_len,
|
||||
G_gpg_vstate.work.io_buffer,
|
||||
&ksz));
|
||||
// send
|
||||
gpg_io_discard(0);
|
||||
gpg_io_inserted(ksz);
|
||||
error = SW_OK;
|
||||
break;
|
||||
|
||||
// --- PSO:DEC:AES
|
||||
case 0x02: {
|
||||
cx_aes_key_t *key;
|
||||
unsigned int sz;
|
||||
key = &G_gpg_vstate.kslot->AES_dec;
|
||||
if (!(key->size != 16)) {
|
||||
THROW(SW_CONDITIONS_NOT_SATISFIED + 5);
|
||||
return SW_CONDITIONS_NOT_SATISFIED;
|
||||
}
|
||||
msg_len = G_gpg_vstate.io_length - G_gpg_vstate.io_offset;
|
||||
sz = cx_aes(key, CX_DECRYPT | CX_CHAIN_CBC | CX_LAST, G_gpg_vstate.work.io_buffer + G_gpg_vstate.io_offset,
|
||||
msg_len, G_gpg_vstate.work.io_buffer, GPG_IO_BUFFER_LENGTH);
|
||||
// send
|
||||
gpg_io_discard(0);
|
||||
gpg_io_inserted(sz);
|
||||
return SW_OK;
|
||||
}
|
||||
case PAD_ECDH:
|
||||
if (G_gpg_vstate.mse_dec->attributes.value[0] != KEY_ID_ECDH) {
|
||||
error = SW_CONDITIONS_NOT_SATISFIED;
|
||||
break;
|
||||
}
|
||||
ecfp_key = &G_gpg_vstate.mse_dec->priv_key.ecfp;
|
||||
gpg_io_fetch_l(&l);
|
||||
gpg_io_fetch_tl(&t, &l);
|
||||
if (t != 0x7f49) {
|
||||
error = SW_WRONG_DATA;
|
||||
break;
|
||||
}
|
||||
gpg_io_fetch_tl(&t, &l);
|
||||
if (t != 0x86) {
|
||||
error = SW_WRONG_DATA;
|
||||
break;
|
||||
}
|
||||
|
||||
// --- PSO:DEC:ECDH
|
||||
case 0xA6: {
|
||||
cx_ecfp_private_key_t *key;
|
||||
unsigned int sz;
|
||||
unsigned int curve;
|
||||
if (G_gpg_vstate.mse_dec->attributes.value[0] != 0x12) {
|
||||
THROW(SW_CONDITIONS_NOT_SATISFIED);
|
||||
return SW_CONDITIONS_NOT_SATISFIED;
|
||||
}
|
||||
key = &G_gpg_vstate.mse_dec->priv_key.ecfp;
|
||||
gpg_io_fetch_l(&l);
|
||||
gpg_io_fetch_tl(&t, &l);
|
||||
if (t != 0x7f49) {
|
||||
THROW(SW_WRONG_DATA);
|
||||
return SW_WRONG_DATA;
|
||||
}
|
||||
gpg_io_fetch_tl(&t, &l);
|
||||
if (t != 0x86) {
|
||||
THROW(SW_WRONG_DATA);
|
||||
return SW_WRONG_DATA;
|
||||
}
|
||||
curve = gpg_oid2curve(G_gpg_vstate.mse_dec->attributes.value + 1,
|
||||
G_gpg_vstate.mse_dec->attributes.length - 1);
|
||||
if (ecfp_key->curve != curve) {
|
||||
error = SW_CONDITIONS_NOT_SATISFIED;
|
||||
break;
|
||||
}
|
||||
if (curve == CX_CURVE_Curve25519) {
|
||||
for (cnt = 0; cnt <= 31; cnt++) {
|
||||
G_gpg_vstate.work.io_buffer[512 + cnt] =
|
||||
(G_gpg_vstate.work.io_buffer + G_gpg_vstate.io_offset)[31 - cnt];
|
||||
}
|
||||
G_gpg_vstate.work.io_buffer[511] = 0x02;
|
||||
CX_CHECK(cx_ecdh_no_throw(ecfp_key,
|
||||
CX_ECDH_X,
|
||||
G_gpg_vstate.work.io_buffer + 511,
|
||||
65,
|
||||
G_gpg_vstate.work.io_buffer + 256,
|
||||
160));
|
||||
CX_CHECK(cx_ecdomain_parameters_length(ecfp_key->curve, &ksz));
|
||||
|
||||
curve = gpg_oid2curve(G_gpg_vstate.mse_dec->attributes.value + 1, G_gpg_vstate.mse_dec->attributes.length - 1);
|
||||
if (key->curve != curve) {
|
||||
THROW(SW_CONDITIONS_NOT_SATISFIED);
|
||||
return SW_CONDITIONS_NOT_SATISFIED;
|
||||
}
|
||||
if (curve == CX_CURVE_Curve25519) {
|
||||
unsigned int i;
|
||||
for (cnt = 0; cnt <= 31; cnt++) {
|
||||
G_gpg_vstate.work.io_buffer[128 + cnt] =
|
||||
G_gpg_vstate.work.io_buffer[287 - cnt];
|
||||
}
|
||||
ksz = 32;
|
||||
} else {
|
||||
CX_CHECK(
|
||||
cx_ecdh_no_throw(ecfp_key,
|
||||
CX_ECDH_X,
|
||||
G_gpg_vstate.work.io_buffer + G_gpg_vstate.io_offset,
|
||||
65,
|
||||
G_gpg_vstate.work.io_buffer + 128,
|
||||
160));
|
||||
CX_CHECK(cx_ecdomain_parameters_length(ecfp_key->curve, &ksz));
|
||||
}
|
||||
// send
|
||||
gpg_io_discard(0);
|
||||
gpg_io_insert(G_gpg_vstate.work.io_buffer + 128, ksz);
|
||||
error = SW_OK;
|
||||
break;
|
||||
|
||||
for (i = 0; i <= 31; i++) {
|
||||
G_gpg_vstate.work.io_buffer[512 + i] = (G_gpg_vstate.work.io_buffer + G_gpg_vstate.io_offset)[31 - i];
|
||||
}
|
||||
G_gpg_vstate.work.io_buffer[511] = 0x02;
|
||||
sz = cx_ecdh(key, CX_ECDH_X, G_gpg_vstate.work.io_buffer + 511, 65, G_gpg_vstate.work.io_buffer + 256, 160);
|
||||
for (i = 0; i <= 31; i++) {
|
||||
G_gpg_vstate.work.io_buffer[128 + i] = G_gpg_vstate.work.io_buffer[287 - i];
|
||||
}
|
||||
sz = 32;
|
||||
} else {
|
||||
sz = cx_ecdh(key, CX_ECDH_X, G_gpg_vstate.work.io_buffer + G_gpg_vstate.io_offset, 65,
|
||||
G_gpg_vstate.work.io_buffer + 128, 160);
|
||||
}
|
||||
// send
|
||||
gpg_io_discard(0);
|
||||
gpg_io_insert(G_gpg_vstate.work.io_buffer + 128, sz);
|
||||
return SW_OK;
|
||||
}
|
||||
// --- PSO:DEC:xx NOT SUPPORTED
|
||||
default:
|
||||
error = SW_REFERENCED_DATA_NOT_FOUND;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
// --- PSO:DEC:xx NOT SUPPORTDED
|
||||
default:
|
||||
THROW(SW_REFERENCED_DATA_NOT_FOUND);
|
||||
return SW_REFERENCED_DATA_NOT_FOUND;
|
||||
//--- PSO:yy NOT SUPPORTED ---
|
||||
default:
|
||||
error = SW_REFERENCED_DATA_NOT_FOUND;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
//--- PSO:yy NOT SUPPPORTED ---
|
||||
default:
|
||||
THROW(SW_REFERENCED_DATA_NOT_FOUND);
|
||||
return SW_REFERENCED_DATA_NOT_FOUND;
|
||||
}
|
||||
THROW(SW_REFERENCED_DATA_NOT_FOUND);
|
||||
return SW_REFERENCED_DATA_NOT_FOUND;
|
||||
end:
|
||||
return error;
|
||||
}
|
||||
|
||||
/**
|
||||
* APDU handler to Internal Authentication
|
||||
*
|
||||
* @return Status Word
|
||||
*
|
||||
*/
|
||||
int gpg_apdu_internal_authenticate() {
|
||||
// --- PSO:AUTH ---
|
||||
if (G_gpg_vstate.kslot->aut.UIF[0]) {
|
||||
if ((G_gpg_vstate.UIF_flags) == 0) {
|
||||
ui_menu_uifconfirm_display(0);
|
||||
return 0;
|
||||
// --- PSO:AUTH ---
|
||||
if (G_gpg_vstate.kslot->aut.UIF[0]) {
|
||||
if ((G_gpg_vstate.UIF_flags) == 0) {
|
||||
ui_menu_uifconfirm_display(0);
|
||||
return 0;
|
||||
}
|
||||
G_gpg_vstate.UIF_flags = 0;
|
||||
}
|
||||
G_gpg_vstate.UIF_flags = 0;
|
||||
}
|
||||
|
||||
if (G_gpg_vstate.mse_aut->attributes.value[0] == 1) {
|
||||
if (G_gpg_vstate.io_length >
|
||||
((G_gpg_vstate.mse_aut->attributes.value[1] << 8) | G_gpg_vstate.mse_aut->attributes.value[2]) * 40 / 100) {
|
||||
THROW(SW_WRONG_LENGTH);
|
||||
return SW_WRONG_LENGTH;
|
||||
if (G_gpg_vstate.mse_aut->attributes.value[0] == KEY_ID_RSA) {
|
||||
if (G_gpg_vstate.io_length > U2BE(G_gpg_vstate.mse_aut->attributes.value, 1) * 40 / 100) {
|
||||
return SW_WRONG_LENGTH;
|
||||
}
|
||||
}
|
||||
}
|
||||
return gpg_sign(G_gpg_vstate.mse_aut);
|
||||
return gpg_sign(G_gpg_vstate.mse_aut);
|
||||
}
|
||||
|
@ -1,42 +0,0 @@
|
||||
/* Copyright 2017 Cedric Mesnil <cslashm@gmail.com>, Ledger SAS
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include "os.h"
|
||||
#include "cx.h"
|
||||
#include "gpg_types.h"
|
||||
#include "gpg_api.h"
|
||||
#include "gpg_vars.h"
|
||||
#include "os_io_seproxyhal.h"
|
||||
|
||||
#if defined(TARGET_NANOX) || defined(TARGET_NANOS2)
|
||||
#include "ux.h"
|
||||
ux_state_t G_ux;
|
||||
bolos_ux_params_t G_ux_params;
|
||||
#else
|
||||
ux_state_t ux;
|
||||
#endif
|
||||
|
||||
#ifndef GPG_DEBUG_MAIN
|
||||
unsigned char G_io_seproxyhal_spi_buffer[IO_SEPROXYHAL_BUFFER_SIZE_B];
|
||||
#else
|
||||
extern unsigned char G_io_seproxyhal_spi_buffer[IO_SEPROXYHAL_BUFFER_SIZE_B];
|
||||
int apdu_n;
|
||||
#endif
|
||||
|
||||
gpg_v_state_t G_gpg_vstate;
|
||||
|
||||
#ifdef HAVE_RSA
|
||||
union cx_u G_cx;
|
||||
#endif // HAVE_RSA
|
@ -1,57 +1,71 @@
|
||||
/* Copyright 2017 Cedric Mesnil <cslashm@gmail.com>, Ledger SAS
|
||||
/*****************************************************************************
|
||||
* Ledger App OpenPGP.
|
||||
* (c) 2024 Ledger SAS.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*****************************************************************************/
|
||||
|
||||
#include "os.h"
|
||||
#include "cx.h"
|
||||
#include "gpg_types.h"
|
||||
#include "gpg_api.h"
|
||||
#include "gpg_vars.h"
|
||||
|
||||
const unsigned char C_MF[] = {0x3F, 0x00};
|
||||
const unsigned char C_ATR[] = {0x2F, 0x02};
|
||||
|
||||
/**
|
||||
* APDU handler to Select the card
|
||||
*
|
||||
* @return Status Word
|
||||
*
|
||||
*/
|
||||
int gpg_apdu_select() {
|
||||
int sw;
|
||||
int sw = SW_UNKNOWN;
|
||||
|
||||
// MF
|
||||
if ((G_gpg_vstate.io_length == 2) && (os_memcmp(G_gpg_vstate.work.io_buffer, C_MF, G_gpg_vstate.io_length) == 0)) {
|
||||
gpg_io_discard(0);
|
||||
sw = SW_OK;
|
||||
}
|
||||
// AID APP
|
||||
else if ((G_gpg_vstate.io_length == 6) &&
|
||||
(os_memcmp(G_gpg_vstate.work.io_buffer, N_gpg_pstate->AID, G_gpg_vstate.io_length) == 0)) {
|
||||
G_gpg_vstate.DO_current = 0;
|
||||
G_gpg_vstate.DO_reccord = 0;
|
||||
G_gpg_vstate.DO_offset = 0;
|
||||
if (G_gpg_vstate.selected == 0) {
|
||||
G_gpg_vstate.verified_pin[0] = 0;
|
||||
G_gpg_vstate.verified_pin[1] = 0;
|
||||
G_gpg_vstate.verified_pin[2] = 0;
|
||||
G_gpg_vstate.verified_pin[3] = 0;
|
||||
G_gpg_vstate.verified_pin[4] = 0;
|
||||
// MF
|
||||
if ((G_gpg_vstate.io_length == sizeof(C_MF)) &&
|
||||
(memcmp(G_gpg_vstate.work.io_buffer, C_MF, G_gpg_vstate.io_length) == 0)) {
|
||||
gpg_io_discard(0);
|
||||
sw = SW_OK;
|
||||
}
|
||||
// EF.ATR
|
||||
else if ((G_gpg_vstate.io_length == sizeof(C_ATR)) &&
|
||||
(memcmp(G_gpg_vstate.work.io_buffer, C_ATR, G_gpg_vstate.io_length) == 0)) {
|
||||
gpg_io_discard(0);
|
||||
sw = SW_OK;
|
||||
}
|
||||
// AID APP
|
||||
else if ((G_gpg_vstate.io_length == 6) && (memcmp(G_gpg_vstate.work.io_buffer,
|
||||
(const void *) N_gpg_pstate->AID,
|
||||
G_gpg_vstate.io_length) == 0)) {
|
||||
G_gpg_vstate.DO_current = 0;
|
||||
G_gpg_vstate.DO_reccord = 0;
|
||||
G_gpg_vstate.DO_offset = 0;
|
||||
if (G_gpg_vstate.selected == 0) {
|
||||
G_gpg_vstate.verified_pin[0] = 0;
|
||||
G_gpg_vstate.verified_pin[1] = 0;
|
||||
G_gpg_vstate.verified_pin[2] = 0;
|
||||
G_gpg_vstate.verified_pin[3] = 0;
|
||||
G_gpg_vstate.verified_pin[4] = 0;
|
||||
}
|
||||
|
||||
gpg_io_discard(0);
|
||||
if (N_gpg_pstate->histo[7] != 0x07) {
|
||||
THROW(SW_STATE_TERMINATED);
|
||||
gpg_io_discard(0);
|
||||
if (N_gpg_pstate->histo[HISTO_OFFSET_STATE] != STATE_ACTIVATE) {
|
||||
sw = SW_STATE_TERMINATED;
|
||||
} else {
|
||||
sw = SW_OK;
|
||||
}
|
||||
}
|
||||
// NOT FOUND
|
||||
else {
|
||||
sw = SW_FILE_NOT_FOUND;
|
||||
}
|
||||
sw = SW_OK;
|
||||
}
|
||||
// NOT FOUND
|
||||
else {
|
||||
THROW(SW_FILE_NOT_FOUND);
|
||||
return SW_FILE_NOT_FOUND;
|
||||
}
|
||||
return sw;
|
||||
return sw;
|
||||
}
|
||||
|
@ -0,0 +1,53 @@
|
||||
|
||||
/*****************************************************************************
|
||||
* Ledger App OpenPGP.
|
||||
* (c) 2024 Ledger SAS.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*****************************************************************************/
|
||||
|
||||
#include "gpg_vars.h"
|
||||
#include "gpg_ux.h"
|
||||
#include "usbd_ccid_if.h"
|
||||
|
||||
/**
|
||||
* Reset CCID
|
||||
*
|
||||
*/
|
||||
void ui_CCID_reset(void) {
|
||||
io_usb_ccid_set_card_inserted(0);
|
||||
io_usb_ccid_set_card_inserted(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Exit app
|
||||
*
|
||||
*/
|
||||
void app_quit(void) {
|
||||
// exit app here
|
||||
os_sched_exit(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset app
|
||||
*
|
||||
*/
|
||||
void app_reset(void) {
|
||||
unsigned char magic[MAGIC_LENGTH];
|
||||
|
||||
explicit_bzero(magic, MAGIC_LENGTH);
|
||||
nvm_write((void*) (N_gpg_pstate->magic), magic, MAGIC_LENGTH);
|
||||
gpg_init();
|
||||
ui_CCID_reset();
|
||||
ui_init();
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
|
||||
/*****************************************************************************
|
||||
* Ledger App OpenPGP.
|
||||
* (c) 2024 Ledger SAS.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*****************************************************************************/
|
||||
|
||||
#ifndef GPG_UX_H
|
||||
#define GPG_UX_H
|
||||
|
||||
#define STR(x) #x
|
||||
#define XSTR(x) STR(x)
|
||||
|
||||
#define LABEL_SIG "Signature"
|
||||
#define LABEL_AUT "Authentication"
|
||||
#define LABEL_DEC "Decryption"
|
||||
|
||||
#define LABEL_RSA2048 "RSA 2048"
|
||||
#define LABEL_RSA3072 "RSA 3072"
|
||||
#define LABEL_RSA4096 "RSA 4096"
|
||||
#define LABEL_SECP256K1 "SECP 256K1"
|
||||
#define LABEL_SECP256R1 "SECP 256R1"
|
||||
#define LABEL_Ed25519 "Ed25519"
|
||||
|
||||
void ui_CCID_reset(void);
|
||||
void app_quit(void);
|
||||
void app_reset(void);
|
||||
void ui_init(void);
|
||||
void ui_menu_pinconfirm_display(unsigned int value);
|
||||
void ui_menu_pinentry_display(unsigned int value);
|
||||
void ui_menu_uifconfirm_display(unsigned int value);
|
||||
|
||||
#endif // GPG_UX_H
|
@ -1,151 +0,0 @@
|
||||
|
||||
/* Copyright 2017 Cedric Mesnil <cslashm@gmail.com>, Ledger SAS
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include "os.h"
|
||||
#include "cx.h"
|
||||
#include "gpg_types.h"
|
||||
#include "gpg_api.h"
|
||||
#include "gpg_vars.h"
|
||||
|
||||
#include "os_io_seproxyhal.h"
|
||||
#include "string.h"
|
||||
#include "glyphs.h"
|
||||
|
||||
/* ----------------------------------------------------------------------- */
|
||||
/* --- Blue UI layout --- */
|
||||
/* ----------------------------------------------------------------------- */
|
||||
/* screeen size:
|
||||
blue; 320x480
|
||||
nanoS: 128x32
|
||||
*/
|
||||
#if 0
|
||||
static const bagl_element_t const ui_idle_blue[] = {
|
||||
{{BAGL_RECTANGLE, 0x00, 0, 60, 320, 420, 0, 0,
|
||||
BAGL_FILL, 0xf9f9f9, 0xf9f9f9,
|
||||
0, 0},
|
||||
NULL,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
NULL,
|
||||
NULL,
|
||||
NULL},
|
||||
|
||||
{{BAGL_RECTANGLE, 0x00, 0, 0, 320, 60, 0, 0,
|
||||
BAGL_FILL, 0x1d2028, 0x1d2028,
|
||||
0, 0},
|
||||
NULL,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
NULL,
|
||||
NULL,
|
||||
NULL},
|
||||
|
||||
{{BAGL_LABEL, 0x00, 20, 0, 320, 60, 0, 0,
|
||||
BAGL_FILL, 0xFFFFFF, 0x1d2028,
|
||||
BAGL_FONT_OPEN_SANS_LIGHT_14px | BAGL_FONT_ALIGNMENT_MIDDLE, 0},
|
||||
"GPG Card",
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
NULL,
|
||||
NULL,
|
||||
NULL},
|
||||
|
||||
{{BAGL_BUTTON | BAGL_FLAG_TOUCHABLE, 0x00, 190, 215, 120, 40, 0, 6,
|
||||
BAGL_FILL, 0x41ccb4, 0xF9F9F9,
|
||||
BAGL_FONT_OPEN_SANS_LIGHT_14px | BAGL_FONT_ALIGNMENT_CENTER | BAGL_FONT_ALIGNMENT_MIDDLE,
|
||||
0},
|
||||
"Exit",
|
||||
0,
|
||||
0x37ae99,
|
||||
0xF9F9F9,
|
||||
gpg_io_seproxyhal_touch_exit,
|
||||
NULL,
|
||||
NULL},
|
||||
|
||||
#ifdef GPG_DEBUG
|
||||
{{BAGL_BUTTON | BAGL_FLAG_TOUCHABLE, 0x00, 20, 215, 120, 40, 0, 6,
|
||||
BAGL_FILL, 0x41ccb4, 0xF9F9F9,
|
||||
BAGL_FONT_OPEN_SANS_LIGHT_14px | BAGL_FONT_ALIGNMENT_CENTER | BAGL_FONT_ALIGNMENT_MIDDLE,
|
||||
0},
|
||||
"Init",
|
||||
0,
|
||||
0x37ae99,
|
||||
0xF9F9F9,
|
||||
gpg_io_seproxyhal_touch_debug,
|
||||
NULL,
|
||||
NULL}
|
||||
#endif
|
||||
|
||||
};
|
||||
|
||||
|
||||
const bagl_element_t ui_idle_nanos[] = {
|
||||
// type
|
||||
{{BAGL_RECTANGLE, 0x00, 0, 0, 128, 32, 0, 0,
|
||||
BAGL_FILL, 0x000000, 0xFFFFFF,
|
||||
0,
|
||||
0},
|
||||
NULL,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
NULL,
|
||||
NULL,
|
||||
NULL},
|
||||
|
||||
{{BAGL_LABELINE, 0x00, 0, 12, 128, 32, 0, 0,
|
||||
0 , 0xFFFFFF, 0x000000,
|
||||
BAGL_FONT_OPEN_SANS_EXTRABOLD_11px|BAGL_FONT_ALIGNMENT_CENTER,
|
||||
0 },
|
||||
"GPGCard",
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
NULL,
|
||||
NULL,
|
||||
NULL },
|
||||
//{{BAGL_LABELINE , 0x02, 0, 26, 128, 32, 0, 0, 0 , 0xFFFFFF, 0x000000, BAGL_FONT_OPEN_SANS_REGULAR_11px|BAGL_FONT_ALIGNMENT_CENTER, 0 }, "Waiting for requests...", 0, 0, 0, NULL, NULL, NULL },
|
||||
|
||||
{{BAGL_ICON , 0x00, 3, 12, 7,
|
||||
7, 0, 0,
|
||||
0 , 0xFFFFFF, 0x000000, 0, BAGL_GLYPH_ICON_CROSS },
|
||||
NULL,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
NULL,
|
||||
NULL,
|
||||
NULL },
|
||||
};
|
||||
|
||||
unsigned int gpg_io_seproxyhal_touch_exit(const bagl_element_t *e) {
|
||||
// Go back to the dashboard
|
||||
os_sched_exit(0);
|
||||
return 0; // do not redraw the widget
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
unsigned int ui_idle_blue_button(unsigned int button_mask,
|
||||
unsigned int button_mask_counter) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
#endif
|
@ -1,37 +1,42 @@
|
||||
/* Copyright 2017 Cedric Mesnil <cslashm@gmail.com>, Ledger SAS
|
||||
/*****************************************************************************
|
||||
* Ledger App OpenPGP.
|
||||
* (c) 2024 Ledger SAS.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*****************************************************************************/
|
||||
|
||||
const char *const C_TEMPLATE_TYPE = "Key type";
|
||||
const char *const C_TEMPLATE_KEY = "Key";
|
||||
const char *const C_TEMPLATE_TYPE = "Key type";
|
||||
const char *const C_TEMPLATE_KEY = "Key";
|
||||
const char *const C_INVALID_SELECTION = "Invalid selection";
|
||||
|
||||
const char *const C_OK = "OK";
|
||||
const char *const C_OK = "OK";
|
||||
const char *const C_NOK = "NOK";
|
||||
|
||||
const char *const C_WRONG_PIN = "PIN Incorrect";
|
||||
const char *const C_RIGHT_PIN = "PIN Correct";
|
||||
const char *const C_WRONG_PIN = "PIN Incorrect";
|
||||
const char *const C_RIGHT_PIN = "PIN Correct";
|
||||
const char *const C_PIN_CHANGED = "PIN changed";
|
||||
const char *const C_PIN_LOCKED = "PIN locked";
|
||||
const char *const C_PIN_DIFFERS = "2 PINs differs";
|
||||
const char *const C_PIN_USER = "User PIN";
|
||||
const char *const C_PIN_ADMIN = "Admin PIN";
|
||||
const char *const C_PIN_USER = "User PIN";
|
||||
const char *const C_PIN_ADMIN = "Admin PIN";
|
||||
|
||||
const char *const C_VERIFIED = "Verified";
|
||||
const char *const C_VERIFIED = "Verified";
|
||||
const char *const C_NOT_VERIFIED = "Not Verified";
|
||||
const char *const C_ALLOWED = "Allowed";
|
||||
const char *const C_NOT_ALLOWED = "Not Allowed ";
|
||||
const char *const C_ALLOWED = "Allowed";
|
||||
const char *const C_NOT_ALLOWED = "Not Allowed ";
|
||||
|
||||
const char *const C_DEFAULT_MODE = "Default mode";
|
||||
|
||||
const char *const C_UIF_LOCKED = "UIF locked";
|
||||
|
||||
const char *const C_EMPTY = "";
|
||||
|
@ -1,25 +0,0 @@
|
||||
|
||||
/* Copyright 2017 Cedric Mesnil <cslashm@gmail.com>, Ledger SAS
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#ifndef GPG_UX_NANOS_H
|
||||
#define GPG_UX_NANOS_H
|
||||
|
||||
void ui_init(void);
|
||||
void ui_main_display(unsigned int value);
|
||||
void ui_menu_pinconfirm_display(unsigned int value);
|
||||
void ui_menu_pinentry_display(unsigned int value);
|
||||
void ui_menu_uifconfirm_display(unsigned int value);
|
||||
#endif
|
@ -0,0 +1,22 @@
|
||||
/*****************************************************************************
|
||||
* Ledger App OpenPGP.
|
||||
* (c) 2024 Ledger SAS.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*****************************************************************************/
|
||||
|
||||
#include "gpg_vars.h"
|
||||
|
||||
gpg_v_state_t G_gpg_vstate;
|
||||
|
||||
const gpg_nv_state_t N_state_pic;
|